0 相关类
public class Link {
class NewNode {
public int value;
public NewNode next;
public NewNode rand;
public NewNode(int value, NewNode next, NewNode rand) {
this.value = value;
this.next = next;
this.rand = rand;
}
public NewNode(int value) {
this.value = value;
}
}
class Node {
public int value;
public Node next;
public Node(int value) {
this.value = value;
}
}
class DoubleNode{
public int value;
public DoubleNode pre;
public DoubleNode next;
public DoubleNode(int value) {
this.value = value;
}
}
public Node head;
public DoubleNode dhead;
public void add(int vlaue) {
Node node = new Node(vlaue);
if (head == null) {
head = node;
} else {
node.next = head;
head = node;
}
}
public void addDoubleNode(int vlaue) {
DoubleNode node = new DoubleNode(vlaue);
if (dhead == null) {
dhead = node;
} else {
node.next = dhead;
dhead.pre =node;
dhead = node;
}
}
// TODO Auto-generated method stub
public static void printLink(Node head) {
System.out.println();
while (head != null) {
System.out.print(head.value + " ");
head = head.next;
}
}
public static void printDLink(DoubleNode head2) {
// TODO Auto-generated method stub
System.out.println();
while (head2 != null) {
System.out.print(head2.value + " ");
head2 = head2.next;
}
}
一 打印两个有序链表的公共部分
给定两个有序链表的头指针head1和head2,打印两个链表的公共部分
要打印两个有序链表的公共部分,可以使用双指针的方法进行比较。
具体实现如下:
public void printCommonPart(Node head1, Node head2) {
System.out.println("Common Part:");
while (head1 != null && head2 != null) {
if (head1.value < head2.value) {
head1 = head1.next;
} else if (head1.value > head2.value) {
head2 = head2.next;
} else {
System.out.println(head1.value + " "); // 打印公共节点
head1 = head1.next;
head2 = head2.next;
}
}
System.out.println();
}
这个方法首先打印出"Common Part:"作为提示信息。
然后使用两个指针 `head1` 和 `head2` 分别指向两个链表的头节点。
在循环中,比较 `head1.value` 和 `head2.value` 的大小关系:
- 如果 `head1.value` 小于 `head2.value`,则移动 `head1` 指针到下一个节点,因为在有序链表中,较小的值一定不会和另一个链表中的值匹配。
- 如果 `head1.value` 大于 `head2.value`,则移动 `head2` 指针到下一个节点,同样因为有序链表中较小的值不会与另一个链表中的值匹配。
- 如果 `head1.value` 等于 `head2.value`,则打印这个公共节点的值,并将两个指针同时移动到各自的下一个节点。
最后,打印一个空行表示结束。
二 在单链表和双链表中删除倒数第K个节点
【题目】分别实现两个函数,一个可以删除单链表中倒数第K个节点,另一个可以删除双链表中倒数第K个节点。
【要求】如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1)。
public void printLink(Node head) {
System.out.println();
while (head != null) {
System.out.print(head.value + " ");
head = head.next;
}
}
public void printDLink(DoubleNode head) {
System.out.println();
while (head != null) {
System.out.print(head.value + " ");
head = head.next;
}
}
public static void main(String[] args) {
Chapter2_2 chapter = new Chapter2_2();
Link link1 = new Link(); // 单链表
Link link2 = new Link(); // 双链表
// 构造链表
for (int i = 10; i > 0; i--) {
link1.add(i);
link2.addDoubleNode(i);
}
chapter.printLink(link1.head); // 打印单链表
Node head = chapter.removeKNode(link1.head, 6); // 删除倒数第6个节点
chapter.printLink(head); // 打印删除后的单链表
chapter.printDLink(link2.dhead); // 打印双链表
DoubleNode dhead = chapter.removeKDoubleNode(link2.dhead, 6); // 删除倒数第6个节点
chapter.printDLink(dhead); // 打印删除后的双链表
}
// 删除双链表中倒数第K个节点
private DoubleNode removeKDoubleNode(DoubleNode head, int k) {
if (head == null || k < 1) {
return null;
}
DoubleNode cur = head;
while (cur != null) {
k--;
cur = cur.next;
}
// 此时k==0,说明原始K等于链表长度;
if (k == 0) {
head = head.next;
head.pre = null; // 将新的头节点的pre设置为null
return head;
}
if (k < 0) {
cur = head;
while (++k != 0) {
cur = cur.next;
}
DoubleNode newNode = cur.next.next;
cur.next = newNode;
if (newNode != null) {
newNode.pre = cur;
}
}
return head;
}
// 删除单链表中倒数第K个节点
private Node removeKNode(Node head, int k) {
if (head == null || k < 1) {
return null;
}
Node cur = head;
while (cur != null) {
k--;
cur = cur.next;
}
if (k == 0) {
head = head.next;
}
if (k < 0) {
cur = head;
while (++k != 0) {
cur = cur.next;
}
cur.next = cur.next.next;
}
return head;
}
三 删除链表的中间节点和a/b处的节点
【题目】给定链表的头节点head,实现删除链表的中间节点的函数。
例如:
不删除任何节点;
1->2,删除节点1;
1->2->3,删除节点2;
1->2->3->4,删除节点2;
1->2->3->4->5,删除节点3;
进阶:给定链表的头节点head、整数a和b,实现删除位于a/b处节点的函数。
例如:链表:1->2->3->4->5,
假设a/b的值为r。如果r等于0,不删除任何节点;如果r在区间(0,1/5]上,删除节点1;
如果r在区间(1/5,2/5]上,删除节点2;
如果r在区间(2/5,3/5]上,删除节点3;
如果r在区间(3/5,4/5]上,删除节点4;
如果r在区间(4/5,1]上,删除节点5;如果r大于1,不删除任何节点。
/**
* 删除链表的中间节点
*
* @param head 链表头节点
* @return 删除中间节点后的链表头节点
*/
public Node removeMidNode(Node head) {
// 链表为空或只有一个节点,不需要删除
if (head == null || head.next == null) {
return null;
}
Node node1 = head;
Node node2 = node1.next.next;
while (node2.next != null && node2.next.next != null) {
node1 = node1.next;
node2 = node2.next.next;
}
// 将中间节点的前一个节点的next指针跳过中间节点,直接指向中间节点的下一个节点
node1.next = node1.next.next;
return head;
}
/**
* 删除位于a/b处节点
*
* @param head 链表头节点
* @param a a的值
* @param b b的值
* @return 删除位于a/b处节点后的链表头节点
*/
public Node removeNodeByRatio(Node head, int a, int b) {
// a小于1或a大于b,不删除任何节点
if (a < 1 || a > b) {
return head;
}
int n = 0;
Node cur = head;
while (cur != null) {
n++;
cur = cur.next;
}
// 计算需要删除的节点位置
n = (int) Math.ceil((double) (a * n) / (double) b);
if (n == 1) {
head = head.next; // 删除头节点
}
if (n > 1) {
cur = head;
while (--n != 1) {
cur = cur.next;
}
cur.next = cur.next.next; // 删除第n个节点
}
return head;
}
/**
* 打印链表
*
* @param head 链表头节点
*/
public void printLink(Node head) {
System.out.println();
while (head != null) {
System.out.print(head.value + " ");
head = head.next;
}
}
// 测试
public static void main(String[] args) {
Chapter2_3 chapter = new Chapter2_3();
Link link1 = new Link();
Link link2 = new Link();
// 构造两个链表
for (int i = 10; i > 0; i--) {
link1.add(i);
link2.add(i);
}
chapter.printLink(link1.head);
Node head1 = chapter.removeMidNode(link1.head);
chapter.printLink(head1);
chapter.printLink(link2.head);
Node head2 = chapter.removeNodeByRatio(link2.head, 5, 18);
chapter.printLink(head2);
}
四 反转单向和双向链表
【题目】分别实现反转单向链表和反转双向链表的函数。
【要求】如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)。
/**
* 反转单向链表
*
* @param head 单向链表头节点
* @return 反转后的单向链表头节点
*/
private Node reverse(Node head) {
Node pre = null; // 前一个节点
Node next = null; // 后一个节点
while (head != null) {
next = head.next; // 保存当前节点的下一个节点
head.next = pre; // 将当前节点的next指针指向前一个节点
pre = head; // 前一个节点指向当前节点
head = next; // 当前节点指向下一个节点
}
return pre; // 返回反转后的头节点
}
/**
* 反转双向链表
*
* @param dhead 双向链表头节点
* @return 反转后的双向链表头节点
*/
private DoubleNode reverseDNode(DoubleNode dhead) {
DoubleNode pre = null; // 前一个节点
DoubleNode next = null; // 后一个节点
while (dhead != null) {
next = dhead.next; // 保存当前节点的下一个节点
dhead.next = pre; // 将当前节点的next指针指向前一个节点
dhead.pre = next; // 将当前节点的prev指针指向下一个节点
pre = dhead; // 前一个节点指向当前节点
dhead = next; // 当前节点指向下一个节点
}
return pre; // 返回反转后的头节点
}
五 反转部分单向链表
【题目】给定一个单向链表的头节点head,以及两个整数from和to,在单向链表上把第from个节点到第to个节点这一部分进行反转。
例如:1->2->3->4->5->null,from=2,to=4调整结果为:1->4->3->2->5->null
再如:1->2->3->null,from=1,to=3调整结果为:3->2->1->null
/**
* 反转部分单向链表
*
* @param head 单向链表头节点
* @param from 反转开始位置
* @param to 反转结束位置
* @return 反转后的单向链表头节点
*/
private Node reversePart(Node head, int from, int to) {
int len = 0; // 链表长度
Node node1 = head; // 当前节点
Node fPre = null; // 反转开始位置节点的前一个节点
Node tPos = null; // 反转结束位置节点的后一个节点
// 计算链表长度,同时找到fPre和tPos节点
while (node1 != null) {
len++;
fPre = len == from - 1 ? node1 : fPre;
tPos = len == to + 1 ? node1 : tPos;
node1 = node1.next;
}
// 判断参数是否合法
if (from > to || from < 1 || to > len) {
return head;
}
// 定位到反转开始位置节点
node1 = fPre == null ? head : fPre.next;
Node node2 = node1.next;
node1.next = tPos;
Node next = null;
// 反转指定部分的链表节点
while (node2 != tPos) {
next = node2.next;
node2.next = node1;
node1 = node2;
node2 = next;
}
// 连接原链表的头部或者fPre的next指向反转后的头节点
if (fPre != null) {
fPre.next = node1;
return head;
}
return node1;
}
六 环形单链表的约瑟夫问题
【题目】
据说著名犹太历史学家Josephus有过以下故事:在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一种自杀方式,41个人排成一个圆圈,由第1个人开始报数,报数到3的人就自杀,然后再由下一个人重新报 1,报数到 3 的人再自杀,这样依次下去,直到剩下最后一个人时,那个人可以自由选择自己的命运。这就是著名的约瑟夫问题。
现在请用单向环形链表描述该结构并呈现整个自杀过程。输入:一个环形单向链表的头节点head和报数的值m。返回:最后生存下来的节点,且这个节点自己组成环形单向链表,其他节点都删掉。
class Node {
int value;
Node next;
public Node(int value) {
this.value = value;
}
}
public class JosephusProblem {
/**
* 约瑟夫问题的解决方法
* @param head 环形单向链表的头节点
* @param m 报数的值
* @return 最后生存下来的节点
*/
public Node josephusKill1(Node head, int m) {
// 首先判断特殊情况:头节点为空,链表只有一个节点或者报数值小于1
if (head == null || head.next == head || m < 1) {
return head; // 返回头节点
}
Node last = head;
// 找到环形单向链表的最后一个节点
while (last.next != head) {
last = last.next;
}
int count = 0; // 计数器,用于计数报数值
// 进行约瑟夫问题的求解
while (head != last) {
if (++count == m) {
last.next = head.next; // 删除报数到指定值的节点
count = 0; // 重置计数器
} else {
last = last.next; // 移动指针到下一个节点
}
head = last.next; // 移动头指针到下一个节点
}
return head; // 返回最后生存下来的节点
}
public static void main(String[] args) {
// 创建一个环形单向链表
Node head = new Node(1);
head.next = new Node(2);
head.next.next = new Node(3);
head.next.next.next = new Node(4);
head.next.next.next.next = new Node(5);
head.next.next.next.next.next = head; // 链表尾部指向头节点,形成环
int m = 3; // 报数的值
JosephusProblem jp = new JosephusProblem();
Node survivor = jp.josephusKill1(head, m); // 调用约瑟夫问题的解决方法
System.out.println("The survivor is: " + survivor.value);
}
}
进阶问题: 如果链表节点数为N,想在时间复杂度为O(N)时完成原问题的要求,该怎么实现?
进阶问题是约瑟夫问题的进一步扩展。在进阶问题中,给定一个环形链表和报数的值m,要求确定最后生存下来的节点。
首先,我们需要判断特殊情况。如果链表为空,链表只有一个节点,或者报数值小于1,那么没有人会被杀死,所以直接返回头节点即可。
然后,我们遍历整个环形链表,计算链表中节点的个数tmp。这是为了确定每次报数的范围。我们需要知道链表中的节点个数,才能确定报数时从哪个位置开始。
接下来,我们调用一个辅助方法getLive来计算最后生存下来的节点位置。该方法使用递归的方式进行计算。
在getLive方法中,我们首先判断特殊情况:如果链表中只有一个节点,那么这个节点肯定会生存下来,直接返回1。
然后,我们利用递归的方式计算生存下来的节点位置。我们将链表中最后生存下来的节点位置记作live。当链表中的节点个数i大于1时,我们根据约瑟夫问题的特性,可以得到递推式:
live = (getLive(i - 1, m) + m - 1) % i + 1
每递归一次,链表中的节点个数就减少1。我们将问题规模缩小,并计算出生存下来的节点位置。
最后,在主方法中,我们创建一个环形链表,并指定报数的值m。然后调用josephusKill2方法,得到最后生存下来的节点。输出该节点的值即可。
总结起来,进阶问题的解决思路是先计算链表中的节点个数,然后利用递归的方式计算出最后生存下来的节点位置。通过不断缩小问题规模和递推式的计算,找到生存下来的节点。
class Node {
int value;
Node next;
public Node(int value) {
this.value = value;
}
}
public class JosephusProblem {
/**
* 约瑟夫问题的解决方法
* @param head 环形单向链表的头节点
* @param m 报数的值
* @return 最后生存下来的节点
*/
public Node josephusKill2(Node head, int m) {
// 首先判断特殊情况:头节点为空,链表只有一个节点或者报数值小于1
if (head == null || head.next == head || m < 1) {
return head; // 返回头节点
}
Node cur = head.next;
int tmp = 1;
// 计算环形单向链表的节点个数
while (cur != head) {
tmp++;
cur = cur.next;
}
// 调用辅助方法,计算生存下来的节点位置
tmp = getLive(tmp, m);
// 移动指针找到生存下来的节点
while (--tmp != 0) {
head = head.next;
}
head.next = head; // 断开其他节点,形成环形链表
return head; // 返回生存下来的节点
}
/**
* 计算生存下来的节点位置
* @param i 环形单向链表的节点个数
* @param m 报数的值
* @return 最后生存下来的节点位置
*/
private int getLive(int i, int m) {
if (i == 1) {
return 1;
}
// 递归计算生存下来的节点位置
return (getLive(i - 1, m) + m - 1) % i + 1;
}
public static void main(String[] args) {
// 创建一个环形单向链表
Node head = new Node(1);
head.next = new Node(2);
head.next.next = new Node(3);
head.next.next.next = new Node(4);
head.next.next.next.next = new Node(5);
head.next.next.next.next.next = head; // 链表尾部指向头节点,形成环
int m = 3; // 报数的值
JosephusProblem jp = new JosephusProblem();
Node survivor = jp.josephusKill2(head, m); // 调用约瑟夫问题的解决方法
System.out.println("The survivor is: " + survivor.value);
}
}
七 判断一个链表是否为回文结构
【题目】
给定一个链表的头节点head,请判断该链表是否为回文结构。例如:
public class PalindromeLinkedList {
/**
* 判断链表是否为回文结构
* @param head 链表头节点
* @return 是否为回文结构
*/
public boolean isPalindrome2(Node head) {
if (head == null || head.next == null) {
return true;
}
Node right = head.next;
Node cur = head;
// 找到链表的中间节点
while (cur.next != null && cur.next.next != null) {
right = right.next; // 右半部分的指针
cur = cur.next.next; // 快指针,两步一次
}
Stack<Node> stack = new Stack<>();
// 将中间节点之后的节点依次入栈
while (right != null) {
stack.push(right);
right = right.next;
}
// 比较节点的值
while (!stack.isEmpty()) {
if (head.value != stack.pop().value) {
return false;
}
head = head.next;
}
return true;
}
}
解决思路:
-
首先,如果链表为空或者只有一个节点,那么它一定是回文结构,直接返回true。
-
接下来,需要找到链表的中间节点。我们使用快指针和慢指针的方法,快指针每次移动两步,慢指针每次移动一步,直到快指针遍历到链表末尾。此时,慢指针指向的节点就是链表的中间节点。注意,如果链表节点个数为奇数,快指针会移动到最后一个节点;如果链表节点个数为偶数,快指针会移动到null节点。
-
得到中间节点之后,我们将中间节点之后的链表节点入栈。这样,栈中的元素顺序和链表后半部分节点的顺序是相反的。
-
接下来,我们要比较链表前半部分和栈中元素的值是否一一对应。我们使用头节点head开始遍历链表前半部分,从栈中弹出元素进行比较。如果有任何一个元素不相等,就返回false;如果所有元素都相等,则返回true,表示链表为回文结构。
该解法的时间复杂度为O(N),其中N是链表的节点个数。因为需要遍历链表一遍,同时入栈的操作也需要遍历一遍链表后半部分。空间复杂度为O(N/2),因为栈中最多存储链表后半部分的节点。
进阶:如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1)。
public class PalindromeLinkedList {
/**
* 判断链表是否为回文结构
* @param head 链表头节点
* @return 是否为回文结构
*/
public boolean isPalindrome3(Node head) {
if (head == null || head.next == null) {
return true;
}
Node n1 = head;
Node n2 = head;
// 查找中间节点
while (n2.next != null && n2.next.next != null) {
n1 = n1.next; // n1指向中间节点
n2 = n2.next.next;
}
n2 = n1.next; // 右半部分的第一个节点
n1.next = null; // 断开链表,将左半部分尾部指向null
Node n3 = null;
// 反转右半部分链表
while (n2 != null) {
n3 = n2.next;
n2.next = n1;
n1 = n2;
n2 = n3;
}
n3 = n1; // n3保存反转后的最后一个节点
n2 = head; // n2指向左半部分的第一个节点
boolean res = true;
// 比较节点的值
while (n1 != null && n2 != null) {
if (n1.value != n2.value) {
res = false;
break;
}
n1 = n1.next;
n2 = n2.next;
}
// 还原链表
n1 = n3.next;
n3.next = null;
while (n1 != null) {
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
}
解决思路:
- 首先,如果链表为空或者只有一个节点,那么它一定是回文结构,直接返回true。
- 接下来,我们要找到链表的中间节点。使用快慢指针的方法,快指针每次移动两步,慢指针每次移动一步,直到快指针达到链表末尾。此时,慢指针指向的节点即为中间节点。注意,如果链表节点个数为奇数,快指针会移动到最后一个节点;如果链表节点个数为偶数,快指针会移动到null节点。
- 然后,将中间节点之后的链表进行反转。使用三个指针n1、n2、n3,其中n1指向当前节点,n2指向下一个节点,n3用于保存下一个节点。通过迭代的方式,将当前节点的next指针指向上一个节点,同时更新指针的位置向后移动。
- 反转完成后,将反转后的右半部分链表的第一个节点记为n3,左半部分链表的第一个节点记为n2,然后分别比较n1和n2处的节点值。如果有不相等的节点值,则链表不是回文结构,返回false;否则,继续比较下一个节点。
- 比较完节点的值后,需要将链表还原。将反转后的右半部分链表再次进行反转,即再次执行步骤3的操作。然后将左半部分链表的尾部指向null,即n3.next=null。
- 最后,返回结果res,表示链表是否为回文结构。
该解法的时间复杂度为O(N),其中N是链表的节点个数。因为需要遍历链表两次,同时进行链表反转。空间复杂度为O(1),只使用常量级的额外空间。
八 将单向链表按某值划分成左边小、中间相等、右边大的形式
【题目】
给定一个单向链表的头节点head,节点的值类型是整型,再给定一个整数pivot。实现一个调整链表的函数,将链表调整为左部分都是值小于pivot的节点,中间部分都是值等于pivot的节点,右部分都是值大于pivot的节点。
除这个要求外,对调整后的节点顺序没有更多的要求。
例如:链表9->0->4->5->1,pivot=3。
调整后链表可以是 1->0->4->9->5,也可以是 0->1->9->5->4。
总之,满足左部分都是小于 3的节点,中间部分都是等于3的节点(本例中这个部分为空),右部分都是大于3的节点即可。对某部分内部的节点顺序不做要求。
/**
* 将单向链表按某值划分成左边小、中间相等、右边大的形式
* @param head 链表头节点
* @param pivot 划分值
* @return 调整后的链表头节点
*/
private Node partition2(Node head, int pivot) {
// 如果链表为空,直接返回head
if (head == null) {
return head;
}
// 计算链表长度
Node cur = head;
int len = 0;
while (cur != null) {
len++;
cur = cur.next;
}
// 将链表节点存入数组中
Node[] nodeArr = new Node[len];
cur = head;
for (int i = 0; i < nodeArr.length; i++) {
nodeArr[i] = cur;
cur = cur.next;
}
// 根据pivot值对节点数组进行划分
arrPartition(nodeArr, pivot);
// 重新连接节点数组中的节点
for (int i = 1; i < nodeArr.length; i++) {
nodeArr[i - 1].next = nodeArr[i];
}
nodeArr[len - 1].next = null;
// 返回调整后的链表头节点
return nodeArr[0];
}
/**
* 根据pivot值对节点数组进行划分
* @param nodeArr 节点数组
* @param pivot 划分值
*/
private void arrPartition(Node[] nodeArr, int pivot) {
int small = -1; // small指针初始指向-1
int big = nodeArr.length; // big指针初始指向数组长度
int index = 0; // 遍历指针
while (index != big) {
if (nodeArr[index].value < pivot) {
// 当前节点值小于pivot,将其与small指针下一位的节点交换,并将small指针和index指针都向右移动一位
swap(nodeArr, ++small, index++);
} else if (nodeArr[index].value == pivot) {
// 当前节点值等于pivot,index指针向右移动一位
index++;
} else {
// 当前节点值大于pivot,将其与big指针前一位的节点交换,并将big指针向左移动一位,index指针不动(因为交换后需要重新判断当前位置的节点值)
swap(nodeArr, --big, index);
}
}
}
/**
* 交换节点数组中的两个节点
* @param nodeArr 节点数组
* @param a 待交换节点的索引a
* @param b 待交换节点的索引b
*/
public void swap(Node[] nodeArr, int a, int b) {
Node tmp = nodeArr[a];
nodeArr[a] = nodeArr[b];
nodeArr[b] = tmp;
}
在原问题的要求之上再增加如下两个要求。
● 在左、中、右三个部分的内部也做顺序要求,要求每部分里的节点从左到右的顺序与原链表中节点的先后次序一致。例如:链表9->0->4->5->1,pivot=3。调整后的链表是0->1->9->4->5。在满足原问题要求的同时,左部分节点从左到右为 0、1。在原链表中也是先出现0,后出现 1;中间部分在本例中为空,不再讨论;右部分节点从左到右为9、4、5。在原链表中也是先出现9,然后出现4,最后出现5。
● 如果链表长度为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)。
public class DivideLinkedList {
/**
* 将单向链表按某值划分成左边小、中间相等、右边大的形式
* @param head 链表头节点
* @param pivot 划分值
* @return 调整后的链表头节点
*/
public Node partitionLinkedList(Node head, int pivot) {
if (head == null || head.next == null) {
return head;
}
Node sH = null; // 小于pivot的头节点
Node sT = null; // 小于pivot的尾节点
Node eH = null; // 等于pivot的头节点
Node eT = null; // 等于pivot的尾节点
Node bH = null; // 大于pivot的头节点
Node bT = null; // 大于pivot的尾节点
// 遍历链表,按照节点的值进行划分
while (head != null) {
Node next = head.next;
head.next = null;
if (head.value < pivot) {
if (sH == null) {
sH = sT = head;
} else {
sT.next = head;
sT = head;
}
} else if (head.value == pivot) {
if (eH == null) {
eH = eT = head;
} else {
eT.next = head;
eT = head;
}
} else {
if (bH == null) {
bH = bT = head;
} else {
bT.next = head;
bT = head;
}
}
head = next;
}
// 拼接三个部分的节点
if (sH != null) {
sT.next = eH; // 小于pivot的尾节点指向等于pivot的头节点
eT = eT == null ? sT : eT; // 更新等于pivot的尾节点
}
if (eT != null) {
eT.next = bH; // 等于pivot的尾节点指向大于pivot的头节点
}
// 返回调整后的链表头节点
return sH != null ? sH : (eH != null ? eH : bH);
}
}
解决思路:
- 初始化小于pivot部分的头节点sH和尾节点sT、等于pivot部分的头节点eH和尾节点eT、大于pivot部分的头节点bH和尾节点bT,初始值都为null。
- 遍历链表,将每个节点根据节点的值分别放入小于pivot部分、等于pivot部分或大于pivot部分。
- 在遍历过程中,需要维护三个部分的头尾节点,在每个部分后面添加新的节点。如果某个部分当前为空,即头节点和尾节点都为null,则将当前节点设为头节点和尾节点;否则,将当前节点添加到尾节点后面,并更新尾节点。
- 遍历完链表后,可以得到三个部分的链表,但是它们的内部节点顺序可能不一致。
- 根据进阶要求,需要拼接三个部分的节点,并保持节点顺序与原链表中的先后次序一致。
- 首先,如果小于pivot的部分不为空,将小于pivot的尾节点指向等于pivot的头节点;如果等于pivot的部分不为空,将等于pivot的尾节点指向大于pivot的头节点。
- 最后,返回调整后的链表的头节点,即小于pivot的头节点(如果不为空),否则返回等于pivot的头节点(如果不为空),否则返回大于pivot的头节点。
九 复制含有随机指针节点的链表
【题目】
一种特殊的链表节点类描述如下:
Node类中的value是节点值,next指针和正常单链表中next指针的意义一样,都指向下一个节点,rand指针是 Node类中新增的指针,这个指针可能指向链表中的任意一个节点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表中所有结构的复制,并返回复制的新链表的头节点。
例如:链表1->2->3->null,假设1的rand指针指向3,2的rand指针指向null,3的rand指针指向1。复制后的链表应该也是这种结构,比如,1′->2′->3′->null,1′的rand指针指向3′,2′的rand指针指向null,3′的rand指针指向1′,最后返回1′。
/**
* 复制含有rand指针的链表
* @param head 原链表头节点
* @return 复制后新链表的头节点
*/
private Node copyNode2(Node head) {
// 使用HashMap存储原节点和复制节点的映射关系
HashMap<Node, Node> map = new HashMap<>();
// 第一次遍历,复制原节点并存入map中
Node cur = head;
while (cur != null) {
map.put(cur, new Node(cur.value)); // 将原节点和对应的复制节点存入map
cur = cur.next;
}
// 第二次遍历,设置复制节点的next和rand指针
cur = head;
while (cur != null) {
// 通过map获取当前节点对应的复制节点,设置复制节点的next和rand指针
map.get(cur).next = map.get(cur.next);
map.get(cur).rand = map.get(cur.rand);
cur = cur.next;
}
// 返回复制后的链表的头节点
return map.get(head);
}
面的代码实现了复制含有rand指针的链表,并使用了HashMap来存储原节点和复制节点的映射关系,从而避免了重复遍历和寻找对应节点的问题。具体实现方法如下:
- 第一次遍历,复制原节点并存入HashMap中。遍历原链表,对于每个原节点,创建一个与之对应的复制节点,并将原节点与对应的复制节点存入HashMap中。
- 第二次遍历,设置复制节点的next和rand指针。再次遍历链表,在每个原节点对应的复制节点上设置next和rand指针分别指向对应的复制节点。
- 返回复制后的链表的头节点,即HashMap中存储的head对应的复制节点。
进阶:不使用额外的数据结构,只用有限几个变量,且在时间复杂度为O(N)内完成原问题要实现的函数。
/**
* 复制含有rand指针的链表
* @param head 原链表头节点
* @return 复制后新链表的头节点
*/
private Node copyNode(Node head) {
if (head == null) {
return null;
}
// 第一步,复制原始节点并将复制节点插入到原节点后面
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;
Node curCopy = null;
while (cur != null) {
next = cur.next.next;
curCopy = cur.next;
curCopy.rand = cur.rand != null ? cur.rand.next : null; // 设置复制节点的rand指针
cur = next;
}
// 第三步,分离原节点和复制节点
Node res = head.next; // 复制节点的头节点
cur = head;
while (cur != null) {
next = cur.next.next;
curCopy = cur.next;
cur.next = next;
curCopy.next = next != null ? next.next : null;
cur = next;
}
return res; // 返回复制后的链表的头节点
}
上面的代码实现了复制含有rand指针的链表,并且在不使用额外数据结构、只使用有限几个变量的前提下,在时间复杂度为O(N)内完成了链表的复制。具体实现方法如下:
- 第一步,复制原始节点并将复制节点插入到原节点后面。遍历原链表,对于每个原节点,复制一个对应的节点,并将其插入到原节点的后面。
- 第二步,设置复制节点的rand指针。遍历链表,设置每个复制节点的rand指针指向对应的原节点的rand指针指向的复制节点。
- 第三步,分离原节点和复制节点。将原节点和复制节点分离成两个链表,分别是原链表和复制后的链表,并返回复制后的链表的头节点。
十 两个单链表生成相加链表
【题目】
假设链表中每一个节点的值都在0~9之间,那么链表整体就可以代表一个整数。
例如:9->3->7,可以代表整数937。
给定两个这种链表的头节点head1和head2,请生成代表两个整数相加值的结果链表。
例如:链表1为9->3->7,链表2为6->3,最后生成新的结果链表为1->0->0->0。
public class AddLinkedList {
/**
* 逆序两个链表并求和
* @param head1 第一个链表的头节点
* @param head2 第二个链表的头节点
* @return 代表两个整数相加值的结果链表的头节点
*/
public Node addLink(Node head1, Node head2) {
// 1. 逆序两个链表
head1 = reverseLink(head1);
head2 = reverseLink(head2);
int n1 = 0; // 链表1当前节点的值
int n2 = 0; // 链表2当前节点的值
int n = 0; // 链表1当前节点值和链表2当前节点值的和
int carry = 0; // 进位
Node node = null; // 当前节点
Node head = null; // 结果链表的头节点
Node head1R = head1; // 链表1的逆序节点
Node head2R = head2; // 链表2的逆序节点
// 3. 相加每个节点并插入结果链表的头部
while (head1R != null || head2R != null) {
// 获取链表1当前节点的值,若链表1已遍历完,则为0
n1 = head1R == null ? 0 : head1R.value;
// 获取链表2当前节点的值,若链表2已遍历完,则为0
n2 = head2R == null ? 0 : head2R.value;
// 计算当前节点的值和进位的和
n = n1 + n2 + carry;
// 创建新的节点并插入结果链表的头部
head = new Node(n % 10);
head.next = node;
node = head;
// 计算进位值
carry = n / 10;
// 遍历链表1和链表2的逆序节点
if (head1R != null) {
head1R = head1R.next;
}
if (head2R != null) {
head2R = head2R.next;
}
}
// 4. 如果还有进位,需要插入一个节点到结果链表的头部
if (carry != 0) {
head = new Node(carry);
head.next = node;
}
// 5. 复原两个链表的顺序
head1 = reverseLink(head1);
head2 = reverseLink(head2);
// 6. 返回结果链表的头节点
return head;
}
/**
* 逆序链表
* @param head 链表的头节点
* @return 逆序后的链表的头节点
*/
public Node reverseLink(Node head) {
Node pre = null; // 前一个节点
Node next = null; // 后一个节点
while (head != null) {
next = head.next; // 记录当前节点的下一个节点
head.next = pre; // 反转当前节点的指针指向前一个节点
pre = head; // 更新前一个节点
head = next; // 更新当前节点
}
return pre; // 返回逆序后的头节点
}
class Node {
public int value;
public Node next;
public Node(int value) {
this.value = value;
}
}
}
上述代码通过逆序两个链表来实现两个整数的相加,最后返回表示相加结果的链表的头节点。具体步骤如下:
- 先逆序链表head1和head2,得到分别逆序后的链表head1R和head2R。
- 初始化相加过程中需要用到的变量:n1、n2存储当前节点值,n存储相加结果,carry存储进位,node和head用于构建结果链表。
- 从头节点开始遍历head1R和head2R,逐个节点进行相加,并将结果节点插入结果链表的头部。
- 如果遍历结束后,还有进位carry不为0,则需插入一个节点到结果链表的头部。
- 最后,为了保持链表数据的完整性,需要将链表head1和head2复原成原来的顺序。
- 返回结果链表的头节点
十一 两个单链表相交的一系列问题
【题目】
在本题中,单链表可能有环,也可能无环。给定两个单链表的头节点 head1和head2,这两个链表可能相交,也可能不相交。请实现一个函数, 如果两个链表相交,请返回相交的第一个节点;如果不相交,返回null 即可。
要求:如果链表1的长度为N,链表2的长度为M,时间复杂度请达到 O(N+M),额外空间复杂度请达到O(1)
public class Problem_11_FindFirstIntersectNode {
public static class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
// 获取相交节点的主方法
public static Node getIntersectNode(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node loop1 = getLoopNode(head1); // 获取链表1的环入口节点(如果有环的话)
Node loop2 = getLoopNode(head2); // 获取链表2的环入口节点(如果有环的话)
if (loop1 == null && loop2 == null) {
return noLoop(head1, head2); // 两个链表都无环的情况
}
if (loop1 != null && loop2 != null) {
return bothLoop(head1, loop1, head2, loop2); // 两个链表都有环的情况
}
return null;
}
// 获取链表的环入口节点
public static Node getLoopNode(Node head) {
if (head == null || head.next == null || head.next.next == null) {
return null;
}
Node slow = head.next; // 慢指针
Node fast = head.next.next; // 快指针
// 快慢指针相遇的位置就是环的入口节点
while (slow != fast) {
if (fast.next == null || fast.next.next == null) {
return null;
}
fast = fast.next.next;
slow = slow.next;
}
fast = head; // 从头节点开始走
// 当快指针和慢指针再次相遇时,就是环的入口节点
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
// 两个无环链表的情况
public static Node noLoop(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node cur1 = head1;
Node cur2 = head2;
int len1 = 0;
int len2 = 0;
// 计算链表1的长度
while (cur1.next != null) {
len1++;
cur1 = cur1.next;
}
// 计算链表2的长度
while (cur2.next != null) {
len2++;
cur2 = cur2.next;
}
if (cur1 != cur2) {
// 若两个链表的尾节点不相同,说明不相交
return null;
}
cur1 = len1 > len2 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
int diff = Math.abs(len1 - len2);
// 让较长的链表先走差值步
while (diff != 0) {
diff--;
cur1 = cur1.next;
}
// 此时同时移动两个指针,相遇的地方即为相交节点
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
// 两个有环链表的情况
public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
Node cur1 = null;
Node cur2 = null;
if (loop1 == loop2) {
cur1 = head1;
cur2 = head2;
int len = 0;
// 计算两个链表到环入口节点的距离
while (cur1 != loop1) {
len++;
cur1 = cur1.next;
}
while (cur2 != loop2) {
len--;
cur2 = cur2.next;
}
cur1 = len > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
len = Math.abs(len);
// 让较长的链表先走差值步
while (len != 0) {
len--;
cur1 = cur1.next;
}
// 此时同时移动两个指针,相遇的地方即为相交节点
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
} else {
cur1 = loop1.next;
// 在环上遍历,若相遇则返回任意一个环的入口节点
while (cur1 != loop1) {
if (cur1 == loop2) {
return loop1;
}
cur1 = cur1.next;
}
return null;
}
}
}
解题思路:
- 先判断两个链表是否有环,若有环则找到各自的环入口节点。
- 分情况讨论:
- 若两个链表都无环,则使用双指针法找到相交的第一个节点。
- 若两个链表都有环,则分别处理找到各自的环入口节点后,再根据相交节点的特性找到相交的第一个节点。
- 若一个链表有环一个链表无环,则不可能相交,直接返回null。
- 分别实现了获取环入口节点、两个无环链表相交、两个有环链表相交的情况处理逻辑。
- 最终返回相交的第一个节点或null。
十二 将单链表的每K个节点之间逆序
题目
给定一个单链表的头节点head,实现函数,使得每K个节点之间逆序,如果最后不够K个节点一组,则不调整最后几个节点。
1->2->3->4->5->6->7->8->null K=3
3->2->1->->6->5->4->7->8->null 7,8不调整,因为不够一组
思路
首先,如果K值小于2,不用进行调整。
方法一:利用栈结构的解法(时间复杂度O(N),额外空间复杂度O(K))
遍历链表,若栈大小不等于K,不断压栈
当栈的大小到达K时,依次弹出并按弹出顺序连接
需要连接好头尾的节点
返回新链表的头节点
public class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
/**
* 对给定的单链表中每K个节点进行逆序
* @param head 给定单链表的头节点
* @param K 每K个节点一组
* @return 逆序后的新头节点
*/
public Node reverseKNodes1(Node head, int K) {
// 如果K小于2则不需要逆序,直接返回头节点
if (K < 2) {
return head;
}
// 使用栈来临时存储节点
Stack<Node> stack = new Stack<Node>();
// 新头节点默认为原头节点
Node newHead = head;
Node cur = head;
Node pre = null;
Node next = null;
// 遍历单链表
while (cur != null) {
next = cur.next; // 保存当前节点的下一个节点
stack.push(cur); // 当前节点入栈
// 当栈中节点数量为K时,进行逆序操作
if (stack.size() == K) {
// 逆序操作,并将逆序后的尾节点作为下一组逆序的头节点的前一个节点保存起来
pre = resign1(stack, pre, next);
// 更新新头节点
newHead = (newHead == head) ? cur : newHead;
}
cur = next; // 处理下一个节点
}
return newHead; // 返回逆序后的新头节点
}
/**
* 逆序给定的节点栈,并根据情况更新前后节点的连接关系
* @param stack 节点栈
* @param left 逆序前的前一个节点
* @param right 逆序后的后一个节点
* @return 当前逆序后的尾节点(下一组逆序的头节点的前一个节点)
*/
public Node resign1(Stack<Node> stack, Node left, Node right) {
Node cur = stack.pop(); // 出栈一个节点作为当前节点
if (left != null) {
left.next = cur; // 更新逆序后的头节点与前一个节点的连接关系
}
Node next = null;
while (!stack.isEmpty()) {
next = stack.pop(); // 依次出栈并更新节点之间的连接关系
cur.next = next;
cur = next;
}
cur.next = right; // 更新逆序后的尾节点与后一个节点的连接关系
return cur; // 返回当前逆序后的尾节点
}
方法二 : 仅是使用指针
这种方法只使用指针来实现对给定单链表中每K个节点进行逆序操作。具体实现思路如下:
- 遍历单链表,依次处理每个节点;
- 记录当前处理的节点以及其下一个节点;
- 当累计节点数等于K时,找到K个节点的前一个节点和后一个节点,调用
resign2
方法进行局部逆序; - 在
resign2
方法中,使用指针实现K个节点的逆序,并更新前一个节点和后一个节点的连接关系。
希望这样详细的注释能够帮助您更好地理解这种解法的实现逻辑。
/**
* 仅使用指针实现对给定单链表中每K个节点进行逆序
* @param head 给定单链表的头节点
* @param k 每K个节点一组
* @return 逆序后的新头节点
*/
public static Node reverseKNodes2(Node head, int k) {
if (head == null || head.next == null || k < 2) {
return head;
}
int count = 0;
Node cur = head;
Node next = null; // 记录k个元素的后一个元素
Node pre = null; // 记录k个元素的前一个元素
Node start = null; // k个元素的首元素
while (cur != null) {
next = cur.next;
count++;
if (count == k) {
start = pre == null ? head : pre.next;
head = pre == null ? cur : head;
// 调用resign2方法进行K个节点的逆序
resign2(pre, start, cur, next);
// 更新pre指向当前K个节点逆序后的尾节点,count重置为0
pre = start;
count = 0;
}
cur = next;
}
return head;
}
/**
* 对指定区间的节点进行逆序
* @param left 指定区间前一个节点
* @param start 指定区间的首节点
* @param end 指定区间的尾节点
* @param right 指定区间后一个节点
*/
public static 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;
}
十三 删除无序单链表中值重复出现的节点
【题目】
给定一个无序单链表的头节点head,删除其中值重复出现的节点。
例如:1->2->3->3->4->4->2->1->1->nul,删除值重复的节点之后为1->2->3->4->null。
请按以下要求实现两种方法。
方法1:如果链表长度为 N,时间复杂度达到O(N)。
方法 2:额外空间复杂度为 O(1)。
// 一、利用栈
public static Node reverseKNodes1(Node head, int k) {
if (head == null || head.next == null || k < 2) return head;
Stack<Node> s = new Stack<Node>(); // 创建一个栈
Node newHead = head; // 初始化新的头节点
Node cur = head; // 当前处理的节点
Node next = null; // 记录压入k个元素后的第 k+1 个元素
Node pre = null; // 记录k个元素的前一个元素
while (cur != null) {
next = cur.next;
s.push(cur); // 当前节点压入栈中
if (s.size() == k) { // 当栈中节点数量达到 k 个时
pre = resign1(s, pre, next); // 调用辅助方法逆序这 k 个节点,并返回更新后的前一个节点
newHead = newHead == head ? cur : newHead; // 更新新的头节点
}
cur = next; // 处理下一个节点
}
return newHead;
}
// 辅助方法,对栈中的节点进行逆序
public static Node resign1(Stack<Node> s, Node left, Node right) {
Node cur = s.pop(); // 弹出栈顶节点
if (left != null) {
left.next = cur; // 若有前一个节点,更新其指向
}
while (!s.isEmpty()) {
cur.next = s.pop(); // 继续弹出节点,并调整指向
cur = cur.next;
}
cur.next = right; // 最后一个弹出的节点指向下一段待处理的节点
return cur; // 返回最后一个节点,作为下一段待处理的前一个节点
}
// 二、仅使用指针
public static Node reverseKNodes2(Node head, int k) {
if (head == null || head.next == null || k < 2) return head;
int count = 0; // 统计当前处理的节点数
Node cur = head; // 当前处理的节点
Node next = null; // 记录k个元素的后一个元素
Node pre = null; // 记录k个元素的前一个元素
Node start = null; // 记录k个元素的首节点
while (cur != null) {
next = cur.next;
count++;
if (count == k) {
start = pre == null ? head : pre.next; // 若前一个节点为空,说明是头部,否则为前一个节点的下一个节点
head = pre == null ? cur : head; // 若前一个节点为空,说明是头部,否则不变
resign2(pre, start, cur, next); // 调用辅助方法逆序这 k 个节点
pre = start; // 更新前一个节点为当前的 start 节点
count = 0; // 统计清零,准备处理下一段节点
}
cur = next; // 处理下一个节点
}
return head;
}
// 辅助方法,逆序给定区间的节点
public static 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; // 若左节点存在,更新其指向为 end
}
start.next = right; // start 节点指向右节点
}
### 解题思路
#### 方法1:利用栈
1. 使用栈来存储节点,当栈的大小达到 k 时,说明需要逆序这 k 个节点。
2. 遍历链表,每次将节点压入栈中,若栈的大小达到 k,则调用辅助方法进行逆序操作,并更新头节点。
3. 重复以上步骤,直到遍历完整个链表。
#### 方法2:仅使用指针
1. 遍历链表,使用两个指针(pre 和 cur)记录需要逆序的区间的前一个节点和当前节点。
2. 使用一个计数器 count 统计当前处理的节点数,当 count 达到 k 时,调用辅助方法进行区间逆序,并更新头节点。
3. 重复以上步骤,直到遍历完整个链表。
这两种方法都是实现单链表的逆序操作,分别使用了栈和指针的方式来实现。方法1通过栈来存储节点,使得逆序操作更加简单;方法2则通过指针的方式,在遍历过程中直接进行逆序操作,省去了额外的存储空间。
十四 在单链表中删除指定值的节点
【题目】
给定一个链表的头节点head和一个整数num,请实现函数将值为num的节点全部删除。
【解答】
方法一:利用栈或其他容器收集节点的方法。时间复杂度为O(N),额外空间复杂度为O(N)。
将值不等于num的节点用栈收集起来,收集完成后重新连接即可。最后将栈底的节点作为新的头节点返回。
public static class Node {
public int data; // 节点值
public Node next; // 下一个节点
public Node(int data) {
this.data = data; // 构造方法,初始化节点值
}
}
/**
* 使用栈收集节点的方法删除指定值的节点
* 时间复杂度:O(N) 空间复杂度:O(N)
*
* @param head 链表的头节点
* @param data 要删除的节点值
* @return 删除指定值节点后的新头节点
*/
public static Node removeNode1(Node head, int data) {
Stack<Node> stack = new Stack<>(); // 创建一个栈用于收集值不等于data的节点
// 遍历链表,将值不等于data的节点压入栈中
while (head != null) {
if (head.data != data) {
stack.push(head);
}
head = head.next;
}
Node newHead = null; // 新的头节点
// 将栈中的节点重新连接
while (!stack.isEmpty()) {
stack.peek().next = newHead; // 将栈中节点指向当前新的头节点
newHead = stack.pop(); // 更新新的头节点为栈顶节点
}
return newHead; // 返回新的头节点
}
方法二:不用任何容器而直接调整的方法。时间复杂度为O(N),额外空间复杂度为O(1)。
*
* 首先从链表头开始,找到第一个值不等于num的节点,作为新的头节点,这个节点是肯定用不删除的,记为newHead。继续往后遍历,
* 假设当前节点为cur,如果cur节点值等于num,就将cur节点删除,删除的方式是将之前最近一个值不等于num的节点pre连接到cur
* 的下一个节点,即pre.next=cur.next;如果cur节点值不等于num,就令pre=cur,即更新最近一个值不等于num的节点。
public static class Node {
public int data; // 节点值
public Node next; // 下一个节点
public Node(int data) {
this.data = data; // 构造方法,初始化节点值
}
}
/**
* 直接删除指定值的节点
* 时间复杂度:O(N) 空间复杂度:O(1)
*
* @param head 链表的头节点
* @param data 要删除的节点值
* @return 删除指定值节点后的新头节点
*/
public static Node removeNode(Node head, int data) {
// 跳过链表头部连续值为data的节点
while (head != null) {
if (head.data != data) {
break;
}
head = head.next;
}
Node pre = head; // 前一个节点
Node cur = head; // 当前处理的节点
// 遍历链表
while (cur != null) {
if (cur.data == data) {
pre.next = cur.next; // 删除值为data的节点
} else {
pre = cur; // 更新前一个节点
}
cur = cur.next; // 处理下一个节点
}
return head; // 返回新的头节点
}
十五 将搜索二叉树转换成双向链表
【题目】
对于二叉树的节点来说,有本身的数据域,有指向左孩子和右孩子的两个指针;对双向链表的节点来说,有本身的数据域,有指向上一 个节点和下一个节点的指针。在结构上,两种结构有相似性,现在有一棵搜索二叉树,请将其转换为一个有序的双向链表。
对每一个节点来说,原来的二叉树右孩子指针等价于转换后的双向链表的next指针,原来的二叉树左孩子指针等价于转换后的pre指针,最后返回转换后的双向链表头节点。
* 【解答】
* 方法一:用队列等容器收集二叉树中序遍历结果的方法。时间复杂度为O(N),额外空间复杂度为O(N),具体过程如下:
* 1、生成一个队列,记为queue,按照二叉树中序遍历的顺序,将每个节点放入queue中。
* 2、从queue中依次弹出节点,并按照弹出的顺序重连所有的节点即可。
* 方法一的具体实现请参看如下代码中的convert1方法。
import java.util.LinkedList;
import java.util.Queue;
public class ConvertBSTToDoubleList1 {
public static class Node {
public int data; // 节点值
public Node left; // 左孩子节点
public Node right; // 右孩子节点
public Node(int data) {
this.data = data; // 构造方法,初始化节点值
}
}
/**
* 将搜索二叉树转换成双向链表
*
* @param head 搜索二叉树的头节点
* @return 转换后的双向链表头节点
*/
public static Node convert1(Node head) {
Queue<Node> queue = new LinkedList<>(); // 创建一个队列,用于存储按中序遍历顺序排列的节点
inOrderToQueue(head, queue); // 将搜索二叉树按中序遍历顺序排列到队列中
if (queue.isEmpty()) {
return head;
}
head = queue.poll(); // 队列头节点作为双向链表头节点
Node pre = head; // 前一个节点
pre.left = null; // 将双向链表头节点的左指针置为空
Node cur = null; // 当前节点
// 遍历队列中的节点,将其构建为双向链表
while (!queue.isEmpty()) {
cur = queue.poll(); // 取出队列中的节点
pre.right = cur; // 前一个节点的右指针指向当前节点
cur.left = pre; // 当前节点的左指针指向前一个节点
pre = cur; // 更新前一个节点为当前节点
}
pre.right = null; // 将双向链表尾节点的右指针置为空
return head; // 返回双向链表头节点
}
/**
* 将搜索二叉树按中序遍历顺序排列到队列中
*
* @param head 当前节点
* @param queue 存储按中序遍历顺序排列的节点的队列
*/
private static void inOrderToQueue(Node head, Queue<Node> queue) {
if (head == null) {
return;
}
inOrderToQueue(head.left, queue); // 处理左子树
queue.offer(head); // 将当前节点排入队列
inOrderToQueue(head.right, queue); // 处理右子树
}
}
方法二:利用递归函数,除此之外不使用任何容器的方法。时间复杂度为O(N),额外空间复杂度为O(h),h为二叉树的高度,具体过
* 程如下:
* 1、实现递归函数process。process的功能是将一棵搜索二叉树转换为一个结构有点特殊的有序双向链表。结构特殊是指这个双向
* 链表尾节点的right指针指向该双向链表的头节点。函数process最终返回这个链表的尾节点。
* process函数的功能是将一棵搜索二叉树变成有序的双向链表,然后让最大值节点的right指针指向最小值节点,最后返回最大值节
* 点。
* 一开始把整棵树的头节点作为参数传进process函数,然后每棵子树都会经历递归函数process的过程,具体过程请参看如下代码中
* 的process方法。
* 为什么要将有序双向链表的尾节点连接头节点之后再返回尾节点呢?因为用这种方式可以快速找到双向链表的头尾两端,从而省去了通
* 过遍历过程才能找到两端的麻烦。
* 2、通过process过程得到的双向链表是尾节点的right指针连向头节点的结构。所以,最终需要将尾节点的right指针设置为null
* 来让双向链表变成正常的样子。
*
* 方法二的具体实现请参看如下代码中的convert2方法。
public class ConvertBSTToDoubleList2 {
public static class Node {
public int data; // 节点值
public Node left; // 左孩子节点
public Node right; // 右孩子节点
public Node(int data) {
this.data = data; // 构造方法,初始化节点值
}
}
/**
* 将搜索二叉树转换成双向链表
*
* @param head 搜索二叉树的头节点
* @return 转换后的双向链表头节点
*/
public static Node convert2(Node head) {
if (head == null) { // 若头节点为空,返回空
return null;
}
Node last = process(head); // 调用process方法,得到转换后的双向链表的尾节点
head = last.right; // 获取双向链表的头节点
last.right = null; // 将双向链表尾节点的右指针置为空
return head; // 返回双向链表头节点
}
/**
* 递归处理节点,将二叉树转换为双向链表
*
* @param head 当前节点
* @return 当前子树转换后的双向链表尾节点
*/
private static Node process(Node head) {
if (head == null) {
return null;
}
Node leftE = process(head.left); // 处理左子树,得到左子树转换后的双向链表尾节点
Node rightE = process(head.right); // 处理右子树,得到右子树转换后的双向链表尾节点
Node leftS = leftE != null ? leftE.right : null; // 左子树转换后的双向链表头节点
Node rightS = rightE != null ? rightE.right : null; // 右子树转换后的双向链表头节点
if (leftE != null && rightE != null) { // 左右子树均不为空
leftE.right = head; // 左子树转换后的双向链表尾节点和当前节点连接
head.left = leftE; // 当前节点的左指针指向左子树转换后的双向链表尾节点
head.right = rightS; // 当前节点和右子树转换后的双向链表头节点连接
rightS.left = head; // 右子树转换后的双向链表头节点的左指针指向当前节点
rightE.right = leftS; // 右子树转换后的双向链表尾节点和左子树转换后的双向链表头节点连接
return rightE; // 返回右子树转换后的双向链表尾节点
} else if (leftE != null) { // 左子树不为空
leftE.right = head; // 左子树转换后的双向链表尾节点和当前节点连接
head.left = leftE; // 当前节点的左指针指向左子树转换后的双向链表尾节点
head.right = leftS; // 当前节点和左子树转换后的双向链表头节点连接
return head; // 返回当前节点
} else if (rightE != null) { // 右子树不为空
head.right = rightS; // 当前节点和右子树转换后的双向链表头节点连接
rightS.left = head; // 右子树转换后的双向链表头节点的左指针指向当前节点
rightE.right = head; // 右子树转换后的双向链表尾节点和当前节点连接
return rightE; // 返回右子树转换后的双向链表尾节点
} else { // 左右子树均为空
head.right = head; // 当前节点的右指针指向自身
return head; // 返回当前节点
}
}
}
十六 单链表的选择排序
【题目】
给定一个无序单链表的头节点head,实现单链表的选择排序。
要求:额外空间复杂度为O(1)。
既然要求额外空间复杂度为O(1),就不能把链表装进数组等容器中排序,排好序之后再重新连接,而是要求面试者在原链表上利用有
* 限几个变量完成选择排序的过程。选择排序是从未排序的部分中找到最小值,然后放在排好序部分的尾部,逐渐将未排序的部分缩小,
* 最后全部变成排好序的部分。本文实现的方法模拟了这个过程。
*
* 1、开始时默认整个链表都是未排序的部分,对于找到的第一个最小值节点,肯定是整个链表的最小值节点,将其设置为新的头节点记为newHead。
2、每次在未排序的部分中找到最小值的节点,把这个节点从未排序的链表中删除,删除的过程当然要保证未排序部分的链表在结构上不至于断开,这就要求我们应该找到要删除节点的前一个节点。
3、把删除的节点(也就是每次的最小值节点)连接到排好序部分的链表尾部。
4、全部过程处理完后,整个链表都已经有序,返回newHead。
和选择排序一样,如果链表的长度为N,时间复杂度为O(N^2),额外空间复杂度为O(1)
public class SelectionSortInList {
// 定义链表节点类
public static class Node {
public int data; // 节点数据
public Node next; // 指向下一个节点的引用
// 节点构造函数
public Node(int data) {
this.data = data;
}
}
// 实现选择排序的方法
public static Node selectionSort(Node head) {
Node tail = null; // 排好序的链表尾部
Node cur = head; // 未排序的链表头部
Node minNodePre; // 最小节点的前驱节点
Node minNode; // 最小节点
while (cur != null) {
minNode = cur; // 当前节点作为最小节点
minNodePre = getMinNodePre(cur); // 获取最小节点的前驱节点
if (minNodePre != null) { // 如果存在最小节点的前驱节点
minNode = minNodePre.next; // 更新最小节点为前驱节点的下一个节点
minNodePre.next = minNode.next; // 移除最小节点
}
// 更新当前节点为非最小节点,如果当前节点是最小节点则指向下一个节点
cur = cur == minNode ? cur.next : cur;
if (tail == null) { // 如果尾部为空,说明是第一次加入节点
head = minNode; // 更新头部为最小节点
} else {
tail.next = minNode; // 将最小节点接到已排序链表尾部
}
tail = minNode; // 更新尾部为最小节点
}
return head; // 返回排序后的链表头部
}
// 获取未排序部分的最小节点的前驱节点
private static Node getMinNodePre(Node head) {
Node minNodePre = null; // 最小节点的前驱节点
Node minNode = head; // 最小节点,默认为头节点
Node pre = head; // 前驱节点,默认为头节点
Node cur = head.next; // 当前节点从头节点的下一个节点开始
while (cur != null) {
if (cur.data < minNode.data) { // 如果当前节点的数据比最小节点小
minNodePre = pre; // 更新最小节点的前驱节点
minNode = cur; // 更新最小节点为当前节点
}
pre = cur; // 更新前驱节点为当前节点
cur = cur.next; // 更新当前节点为下一个节点
}
return minNodePre; // 返回最小节点的前驱节点
}
}
十七 一种怪异的节点删除方式
*
* 【题目】
* 链表节点值类型为int型,给定一个链表中的节点node,但不给定整个链表的头节点。如何在链表中删除node?请实现这个函数,并
* 分析这么会出现哪些问题。
* 要求:时间复杂度为O(1)。
*
* 【难度】
* 简单
*
* 【解答】
* 本题的思路很简单,举例就能说明具体的做法。
* 例如,链表1->2->3->null,只知道要删除节点2,而不知道头节点。那么只需把节点2的值变成节点3的值,然后在链表中删除节
* 点3即可。
*
* 这道题目出现的次数很多,这么做看起来非常方便,但其实是有很大问题的。
*
* 问题一:这样的删除方式无法删除最后一个节点。还是以原示例来说明,如果知道要删除节点3,而不知道头节点。但它是最后的节
* 点,根本没有下一个节点来代替节点3被删除,那么只有让节点2的next指向null这一种办法,而我们又根本找不到节点2,所以根
* 本没法正确删除节点3。读者可能会问,我们能不能把节点3在内存上的区域变成null呢?这样不就相当于让节点2的next指针指向
* 了null,起到节点3被删除的效果了吗?不可以。null在系统中是一个特定的区域,如果想让节点2的next指针指向null,必须找
* 到节点2。
*
* 问题二:这种删除方式在本质上根本就不是删除了node节点,而是把node节点的值改变,然后删除node的下一个节点,在实际的工程
* 中可能会带来很大问题。比如,工程上的一个节点可能代表很复杂的结构,节点值的复制会相当复杂,或者可能改变节点值这个操作都
* 是被禁止的;再如,工程上的一个节点代表提供服务的一个服务器,外界对每个节点都有很多依赖,比如,示例中删除节点2时,其实
* 影响了节点3对外提供的服务。
public class LinkedListNodeDeletion {
// 定义链表节点类
public static class Node {
public int data; // 节点的数据
public Node next; // 指向下一个节点的指针
/**
* 构造函数,用于初始化节点
*
* @param data 节点的数据
*/
public Node(int data) {
this.data = data;
}
}
/**
* 删除指定节点的方法
*
* @param node 要删除的节点
* @throws RuntimeException 如果要删除的节点为最后一个节点,则抛出运行时异常
*/
public static void removeNode(Node node) {
// 如果要删除的节点为空,直接返回,不做任何操作
if (node == null) {
return;
}
// 获取要删除节点的后继节点
Node next = node.next;
// 如果后继节点为空,即要删除的节点为最后一个节点,抛出异常
if (next == null) {
throw new RuntimeException("Can not remove last node.");
}
// 将要删除节点的值替换为其后继节点的值
node.data = next.data;
// 将要删除节点的指针指向其后继节点的后继节点,相当于删除了该节点
node.next = next.next;
}
}
十八 向有序的环形单链表中插入新节点
【题目】
一个环形单链表从头节点head开始不降序,同时由最后的节点指回头节点。给定这样一个环形单链表的头节点head和一个整数num,
请生成节点值为num的新节点,并插入到这个环形链表中,保证调整后的链表依然有序。
* 【解答】
* 直接给出时间复杂度为O(N)、额外空间复杂度为O(1)的方法。具体过程如下:
* 1、生成节点值为num的新节点,记为node。
* 2、如果链表为空,让node自己组成环形链表,然后直接返回node。
* 3、如果链表不为空,令变量pre=head,cur=head.next,然后令pre和cur同步移动下去,如果遇到pre的节点值小于或等于
* num,并且cur的节点值大于或等于num,说明node应该在pre节点和cur节点之间插入,插入node,然后返回head即可。
* 4、如果pre和cur转了一圈,这期间都没有发现步骤3所说的情况,说明node应该插入到头节点的前面,这种情况之所以会发生,要
* 么是因为node节点的值比链表中每个节点的值都大,要么是因为node的值比链表中每个节点的值都小。
* 5、如果node节点的值比链表中每个节点的值都大,返回原来的头节点即可;如果node节点的值比链表中每个节点的值都小,应该把
* node作为链表新的头节点返回。
public class InsertNodeInOrderedCircularList {
// 定义环形链表节点类
public static class Node {
public int data; // 节点的数据
public Node next; // 指向下一个节点的指针
/**
* 构造函数,用于初始化节点
*
* @param data 节点的数据
*/
public Node(int data) {
this.data = data;
}
}
/**
* 向有序的环形单链表中插入新节点
*
* @param head 头节点
* @param data 要插入的节点值
* @return 调整后的链表的头节点
*/
public static Node insert(Node head, int data) {
// 创建新节点
Node node = new Node(data);
// 如果链表为空,则新节点成为头节点,并形成循环
if (head == null) {
node.next = node;
return node;
}
// 初始化指针,prev指向当前节点的前一个节点,cur指向当前节点
Node pre = head;
Node cur = head.next;
// 遍历链表,找到合适的位置插入新节点
while (cur != head) {
if (pre.data <= data && cur.data >= data) {
break;
}
pre = cur;
cur = cur.next;
}
// 插入新节点
pre.next = node;
node.next = cur;
// 如果新节点插入到了尾节点之前,则不需要调整头节点
if (head.data < data) {
return head;
} else {
return node; // 新节点成为了新的头节点
}
}
}
原文链接:https://blog.csdn.net/troubleshooter/article/details/122509910
十九 合并两个有序的单链表
【题目】
给定两个有序单链表的头节点head1和head2,请合并两个有序链表,合并后的链表依然有序,并返回合并后链表的头节点。
【解答】
* 本题比较简单,假设两个链表的长度分别为M和N,直接给出时间复杂度为O(M+N),额外空间复杂度为O(1)的方法。具体过程如下:
*
* 1、如果两个链表中有一个为空,说明无须合并过程,返回另一个链表的头节点即可。
* 2、比较head1和head2的值,小的节点也是合并后链表的最小节点,这个节点无疑应该是合并链表的头节点,记为head;在之后的
* 步骤里,哪个链表的头节点的值更小,另一个链表的所有节点都会依次插入到这个链表中。
* 3、不妨设head节点所在的链表为链表1,另一个链表为链表2。链表1和链表2都从头部开始一起遍历,比较每次遍历到的两个节
* 点的值,记为cur1和cur2,然后根据大小关系做出不同的调整,同时用一个变量pre表示上次比较时值较小的节点。
* 4、如果链表1先走完,此时cur1=null,pre为链表1的最后一个节点,那么就把pre的next指针指向链表2当前的节点
* (即cur2),表示把链表2没遍历到的有序部分直接拼接到最后,调整结束。如果链表2先走完,说明链表2的所有节点都已经插入
* 到链表1中,调整结束。
* 5、返回合并后链表的头节点head。
public class MergeTwoOrderedSingleList {
// 定义链表节点类
public static class Node {
public int data; // 节点的数据
public Node next; // 指向下一个节点的指针
/**
* 构造函数,用于初始化节点
*
* @param data 节点的数据
*/
public Node(int data) {
this.data = data;
}
}
/**
* 合并两个有序的单链表
*
* @param head1 第一个链表的头节点
* @param head2 第二个链表的头节点
* @return 合并后链表的头节点
*/
public static Node merge(Node head1, Node head2) {
// 如果其中一个链表为空,则直接返回另一个链表
if (head1 == null || head2 == null) {
return head1 != null ? head1 : head2;
}
// 确定合并后链表的头节点
Node head = head1.data < head2.data ? head1 : head2;
// 初始化指针
Node cur1 = head == head1 ? head1 : head2; // 指向第一个链表的当前节点
Node cur2 = head == head1 ? head2 : head1; // 指向第二个链表的当前节点
Node pre = null; // 用于记录上一个节点
// 开始合并两个链表
while (cur1 != null && cur2 != null) {
if (cur1.data <= cur2.data) {
// 如果第一个链表的节点值小于等于第二个链表的节点值,则继续遍历第一个链表
pre = cur1;
cur1 = cur1.next;
} else {
// 如果第一个链表的节点值大于第二个链表的节点值,则将第二个链表的当前节点接入到第一个链表中
Node next = cur2.next; // 保存第二个链表的下一个节点
pre.next = cur2; // 将第二个链表的当前节点接入到第一个链表中
cur2.next = cur1; // 更新第二个链表的当前节点的下一个节点为第一个链表的当前节点
pre = cur2; // 更新上一个节点为当前节点
cur2 = next; // 移动第二个链表的当前节点到下一个节点
}
}
// 将剩余的非空链表直接接入到合并后链表的末尾
pre.next = cur1 == null ? cur2 : cur1;
return head; // 返回合并后链表的头节点
}
}
原文链接:https://blog.csdn.net/troubleshooter/article/details/122513566
二十 按照左右半区的方式重新组合单链表
【题目】
* 给定一个单链表的头部节点head,链表长度为N,如果N为偶数,那么前N/2个节点算作左半区,后N/2个节点算作右半区;如果N为奇
* 数,那么前N/2个节点算作左半区,后N/2+1个节点算作右半区。左半区从左到右依次记为L1->L2->...,右半区从左到右依次记为
* R1->R2->...,请将单链表调整成L1->R1->L2->R2->...的形式。
*
* 【难度】
* 简单
*
* 【解答】
* 假设链表的长度为N,直接给出时间复杂度为O(N)、额外空间复杂度为O(1)的方法,具体过程如下:
*
* 1、如果链表长度为空或长度为1,不用调整,过程直接结束。
* 2、链表长度大于1时,遍历一遍找到左半区的最后一个节点,记为mid。
* 3、遍历一遍找到mid之后,将左半区与右半区分离成两个链表(mid.next=null),分别记为left(head)和right(原来的
* mid.next)。
* 4、将两个链表按照题目要求合并起来。
*
* 具体过程请参看如下代码中的recombine方法,其中的mergeLeftRightPart方法为步骤4的合并过程。
原文链接:https://blog.csdn.net/troubleshooter/article/details/122513943
public class RecombineSingleListByLeftRightPart {
// 定义链表节点类
public static class Node {
public int data; // 节点的数据
public Node next; // 指向下一个节点的指针
/**
* 构造函数,用于初始化节点
*
* @param data 节点的数据
*/
public Node(int data) {
this.data = data;
}
}
/**
* 按照左右半区的方式重新组合单链表
*
* @param head 单链表的头节点
*/
public static void recombine(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;
}
// 划分左右两部分链表
right = mid.next; // 右半区的头节点
mid.next = null; // 将左半区的尾节点指向null,断开左右两部分的连接
// 合并左右两部分链表
mergeLeftRightPart(head, right);
}
/**
* 合并左右两部分链表
*
* @param left 左半区链表的头节点
* @param right 右半区链表的头节点
*/
private static void mergeLeftRightPart(Node left, Node 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; // 将剩余的右半区接到整个链表的末尾
}
}