文章目录
1. 打印两个有序链表的公共部分
给定两个有序链表的头指针head1和head2,打印两个链表的公共部分
解题思路
因为链表是有序的,可以从链表的头结点开始比较,谁小谁就下移,相等的时候,判断结点是否也相等,如果也相等的话,就是公共部分的头结点。
代码
public class Code_10_PrintCommonListPart {
public static class Node{
int val;
Node next;
public Node(int val){
this.val = val;
}
}
public static void printCommonPart(Node head1, Node head2){
Node p1 = head1;
Node p2 = head2;
while(p1 != null && p2 != null){
if(p1.val > p2.val){
p2 = p2.next;
}else if(p1.val < p2.val){
p1 = p1.next;
}else{
if(p1 == p2){
while(p1 != null){
System.out.println(p1.val);
p1 = p1.next;
}
return;
}
p1 = p1.next;
p2 = p2.next;
}
}
}
2. 反转单链表
反转单链表,如果链表长度为N,要求时间复杂度要求为O(N),额外空间复杂度要求为O(1)。
解题思路
定义一个pre,指向当前节点的前一个结点,一个temp,用来存储当前结点的下一个结点,一个cur,指向当前结点,首先存储当前结点的下一个结点,然后,将当前结点的下一个结点改为pre,再把当前结点赋给pre,将temp结点赋给cur,最后pre就是反转之后的链表的头结点。
代码
public static Node reverseSingleList(Node head){
if(head == null || head.next == null) return head;
Node cur = head;
Node pre = null;
Node temp = null;
while(cur != null){
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
3. 反转双链表
反转双链表,如果链表长度为N,要求时间复杂度要求为O(N),额外空间复杂度要求为O(1)。
解题思路
和反转单链表类似,定义一个指针p指向当前结点的前一个,一个n指向当前结点的下一个结点,首先保存当前结点的下一个结点,让结点的pre指向结点的下一个结点n,让当前结点的next指向当前结点的前一个结点,最后将当前结点赋给p,将当前结点的下一个结点n赋给cur。
代码
public static class DoubleNode{
//结点的前一个结点
public DoubleNode pre;
//结点的下一个结点
public DoubleNode next;
public int val;
public DoubleNode(int val){
this.val = val;
}
}
public static DoubleNode reverseDoubleList(DoubleNode head){
if(head == null || head.next == null) return head;
DoubleNode n = null;
DoubleNode p = null;
DoubleNode cur = head;
while(cur != null){
n = cur.next;
cur.pre = n;
cur.next = p;
p = cur;
cur = n;
}
return p;
}
4. 判断一个链表是否是回文结构
给定一个链表的头节点head,请判断该链表是否为回文结构。
例如:
1->2->1,返回true。
1->2->2->1,返回true。
15->6->15,返回true。
1->2->3,返回false。
解题思路
最简单的解法: 定义一个栈,将结点存到栈中,然后依次弹出结点,并与链表中的结点比较,如果值都相等,那么链表是回文结构,只要有一个不相等,就不是回文结构。
代码
// Node 类省略
public static boolean isPalindromeList(Node head){
if(head == null || head.next == null) return true;
Deque<Integer> stack = new LinkedList<>();
Node p = head;
Node p2 = head;
while(p != null){
stack.push(p.val);
p = p.next;
}
while(p2 != null){
if(p2.val != stack.pop()) return false;
p2 = p2.next;
}
return true;
}
上面方法可以改进,可以让指定两个快慢指针,快指针一次走两步,慢指针一次走一步,当快指针走到头时,慢指针在中间位置,把后半部分压入栈,拿这部分和前半部分比较,实际上时省一半空间但空间复杂度还是O(n)。注意,这里是把后半部分压入栈,而不是把前半部分压入栈,是因为当为偶数个结点时,慢指针是向下取中间结点,如果把前半部分压入栈的话,栈中元素比后半部分多一个元素,不好比较,而且,结点时奇数时,正好可以比较,所以,又要分两种情况,比较麻烦。
代码
public static boolean isPalindromeList2(Node head){
if(head == null || head.next == null) return true;
Node quick = head;
Node slow = head;
Node p = head;
Deque<Integer> stack = new LinkedList<>();
//找到中间结点,如果结点是偶数,向下取结点,比如有四个结点,找到的中间结点是第三个结点
//如果是偶数个结点的话,找到的正好是中间结点
while(quick != null && quick.next != null){
quick = quick.next.next;
slow = slow.next;
}
//将后半部分存到栈中
while(slow != null){
stack.push(slow.val);
slow = slow.next;
}
//将栈中的元素和链表左半部分比较
while(!stack.isEmpty()){
if(stack.pop() != p.val) return false;
p = p.next;
}
return true;
}
4.1 进阶
如果链表长度为N,要求时间复杂度达到O(N),额外空间复杂度达到O(1)。
解题思路
使用快慢指针,当快指针到头的时候,将最后一个结点到中间结点这一部分给逆过来,比较从头结点到中间结点这一步分结点是否和从尾结点到中间结点这一部分结点相等,如果都相等的话,就是回文的。最后,不要忘记把链表再给逆过来。
代码
public static boolean isPalindromeList3(Node head){
if(head == null || head.next == null) return true;
Node slow = head;
Node quick = head;
Node p1 = head;
while(quick != null && quick.next != null){
quick = quick.next.next;
slow = slow.next;
}
//反转链表(中间结点到尾结点)
Node p2 = reverse(slow);
Node p = p2;
boolean res = true;
//比较左半部分和与半部分是否都相等
while(p2 != null && p1 != null){
if(p1.val != p2.val) res = false;
p1 = p1.next;
p2 = p2.next;
}
//最后把反转的链表再反转过来
reverse(p);
return res;
}
//反转链表
private static Node reverse(Node head){
Node cur = head;
Node pre = null;
Node temp = null;
while(cur != null){
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
5. 划分链表
给定一个单向链表的头节点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的节点即可。对某部分内部的节点顺序不做 要求。
解题思路
简单解法: 首先计算出结点的个数,然后,定义一个数组,将结点存到数组中,注意将结点的next设为null,然后执行partition过程,类似快速排序,将数组中的结点,按小于target、等于target、大于target的顺序排好,最后,将结点连成一个链表即可。
public class Code_12_DevideList {
public static class Node{
int val;
Node next;
public Node(int val){
this.val = val;
}
}
public static Node devide01(Node head, int target){
Node cur = head;
int count = 0;
while(cur != null){
count++;
cur = cur.next;
}
Node[] nodeArr = new Node[count];
cur = head;
Node temp = null;
for(int i = 0; i < nodeArr.length; i++){
temp = cur.next;
//把当前结点的next设为null
cur.next = null;
nodeArr[i] = cur;
cur = temp;
}
arrPartition(nodeArr, target);
int i = 0;
for( i = 0; i < nodeArr.length - 1; i++){
nodeArr[i].next = nodeArr[i + 1];
}
nodeArr[i].next = null;
return nodeArr[0];
}
/*partition思路
定义一个变量index,index从数组第一个元素开始,定义small、big,分别为小于等于
target的、大于target的边界,small从-1开始,big从数组的长度位置开始,当arr[index]小于target,交换index
和++small(small的后一个值),当arr[index]大于target,交换index和--big(big的前
一个值),等于的时候,index++,不需要交换,注意小于的时候index++,大于的时候,index不变
*/
private static void arrPartition(Node[] nodeArr, int target) {
int small = -1;
int big = nodeArr.length;
int index = 0;
while(index < big){
if(nodeArr[index].val < target){
swap(nodeArr, ++small, index++);
}else if(nodeArr[index].val > target){
swap(nodeArr, --big, index);
}else{
index++;
}
}
}
private static void swap(Node[] nodeArr, int a, int b) {
Node temp = nodeArr[a];
nodeArr[a] = nodeArr[b];
nodeArr[b] = temp;
}
}
5.1 进阶
在原问题的要求之上再增加如下两个要求。
在左、中、右三个部分的内部也做顺序要求,要求每部分里的节点从左 到右的顺序与原链表中节点的先后次序一致。
例如:链表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)。
解题思路
定义六个指针,s、s1,分别指向小于target的头部和尾部,e、e1,分别指向等于target的结点的头部和尾部,b、b1,分别指向大于target的结点的头部和尾部。遍历结点,当一个结点node小于target时,判断s是否为空,为空的话,就让s=node,不为空的话,然后让s1.next=node,并且s1=node,等于大于target时 也一样,注意要让node的next为空。最后,连接,判断s1是否为空,不为空的话,连接等于target部分的头结点e,并且判断e1是否为空,为空的话,就将s1赋给e1,之后判断e1是否为空(如果s1不为空,那么判断肯定成立),不为空的话,让e1.next=b,最后返回的头结点的时候,判断s是否为空,不为空,直接返回,为空,判断e是否为空,不为空,直接返回,为空,返回b。
public static Node devide(Node head, int target) {
Node s = null;
Node s1 = null;
Node e = null;
Node e1 = null;
Node b = null;
Node b1 = null;
//定义一个temp变量来保存head的下一个结点
Node temp = null;
while (head != null) {
temp = head.next;
//将结点放到对应的位置
if (head.val < target) {
//判断头结点是否为空,如果为空,就让这个结点作为头结点
//如果不为空,就放在末尾,并且指针指向最后一个元素
if (s == null) {
s = head;
s1 = head;
} else {
s1.next = head;
s1 = head;
}
} else if (head.val > target) {
if (b == null) {
b = head;
b1 = head;
} else {
b1.next = head;
b1 = head;
}
}else {
if (e == null) {
e = head;
e1 = head;
} else {
e1.next = head;
e1 = head;
}
}
//必须要将head.next设为空,否则连接的时候会产生循环链表
head.next = null;
head = temp;
}
//连接小于等于的部分
if(s1 != null){
s1.next = e;
//如果等于的部分为空,就把s1赋给e1,使它不为空,使下面判断成立
e1 = e1 == null ? s1 : e1;
}
//连接所有
if(e1 != null){
e1.next = b;
}
//返回头结点时,先判断s是否为空,为空的话,就判断e是否为空,e也为空的话,直接但会b
return s != null ? s : e != null ? e : b;
}
6. 复制含有随机指针节点的链表
【题目】
一种特殊的链表节点类描述如下:
public class Node {
public int value;
public Node next;
public Node rand;
public Node(int data) {
this.value = data;
}
}
Node类中的value是节点值,next指针和正常单链表中next指针的意义一 样,都指向下一个节点,rand指针是Node类中新增的指针,这个指针可 能指向链表中的任意一个节点,也可能指向null。 给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个 函数完成这个链表中所有结构的复制,并返回复制的新链表的头节点。
解题思路
定义一个map,以老的Node作为key,以新的Node(复制的Node)作为value,然后,再遍历链表,根据Node的关系指定新Node 之间的关系。
public static Node copyList1(Node head){
if(head == null) return null;
Map<Node, Node> map = new HashMap<>();
Node cur = head;
while(cur != null){
map.put(cur, new Node(cur.value));
cur = cur.next;
}
cur = head;
while(cur != null){
map.get(cur).rand = map.get(cur.rand);
map.get(cur).next = map.get(cur.next);
cur = cur.next;
}
return map.get(head);
}
6.1 进阶
不使用额外的数据结构,只用有限几个变量,且在时间复杂度为 O(N)内完成原问题要实现的函数。
解题思路
使用map主要是为了保存新结点,可以把新节点放在老结点的后面,然后,根据老节点中的rand关系,指定新结点的rand关系,最后将新节点和老结点分离出来即可。
public static Node copyList2(Node head){
Node temp = null;
Node cur = head;
//复制结点并将新结点连在老结点后面
while(cur != null){
temp = cur.next;
cur.next = new Node(cur.value);
cur.next.next = temp;
cur = temp;
}
cur = head;
Node curCopy = null;
//根据老结点中的rand关系指定新结点的rand关系
while(cur != null){
curCopy = cur.next;
//新结点的rand结点是老结点的rand结点的下一个结点(新节点)
curCopy.rand = cur.rand != null ? cur.rand.next : null;
cur = cur.next.next;
}
//新老结点分离
Node res = head.next;
cur = head;
while(cur != null){
temp = cur.next.next;
curCopy = cur.next;
//恢复老链表
cur.next = temp;
//新结点的next结点是老结点的next结点的下一个结点(新节点)
curCopy.next = temp != null ? temp.next : null;
cur = temp;
}
return res;
}
7. 两个单链表相交的一系列问题
在本题中,单链表可能有环,也可能无环。给定两个单链表的头节点 head1和head2,这两个链表可能相交,也可能不相交。请实现一个函数, 如果两个链表相交,请返回相交的第一个节点;如果不相交,返回null 即可。
要求:如果链表1的长度为N,链表2的长度为M,时间复杂度请达到 O(N+M),额外空间复杂度请达到O(1)。
解题思路
- 首先判断链表是否有环,最简单的方法是使用一个HashSet,只要有重复的结点,就说明该链表有环,这种方法空间复杂度为O(n),还有一种方法,就是定义一个快慢指针,快指针一次走两步,慢指针一次走一步,如果快指针等于null了,说明,链表没有环,如果有环,快指针和慢指针,肯定会相遇,相遇的时候,让快指针从头结点开始下移,并且一次直走一步,下次相遇的结点,就是环的起点。
- 两个链表,有三种情况,要么都无环,要么都有环,要么一个有环,一个无环,最后一种情况,两个链表肯定不相交。
- 无环的情况,找相交的结点,先遍历两条链表,算出结点的个数,让结点多的先下移多的结点数,然后,让两个链表同时下移,如果结点相等,那么相等的结点就是两条链表相交的结点,如果结点都不相等,那么两条链表就不相交。
- 有环的情况,又可以分为两种情况,一种是两条链表在环外相交,交点在环外,另一种是焦点在环内,这样的话,返回任意一个链表的环的起点都可。判断两个环的起点是否相等,相等的话,就是第一种情况,这样的话,就是相当于无环的情况。不相等的话,遍历一个链表,如果结点中有和另一个链表环的起点相等的点,说明是第二种情况,返回任意一个链表环的起点都可以。
代码
public static Node findFirstIntersectNode(Node head1, Node head2){
Node n1 = getLoopNode(head1);
Node n2 = getLoopNode(head2);
//如果两条链表都无环,就是n1、n2都为null
if(n1 == null && n2 == null){
//判断两条无环链表是否相交,如果相交,同时算出相交的结点
return NoLoopNode(head1, head2);
}else if(n1 != null && n2 != null){
/*如果两条链表都有环,那么,又有三种情况,两条链表不相交,但是
两条链表都有环,两条有环链表相交的结点不在环上,在环外,最后一种是
两条链表的相交结点不在环外,在环内,这样的话,相交结点不止一个
*/
return bothLoopNode(head1, n1, head2, n2);
}else{
return null;
}
}
//判断链表是否有环,有的话返回环的第一个节点,没有返回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 == null || fast.next == null ) return null;
fast = fast.next.next;
slow = slow.next;
}
fast = head;
while(fast != slow){
fast = fast.next ;
slow = slow.next;
}
return fast;
}
//判断两个无环链表是否相交,相交返回相交的第一个节点,不相交返回null
private static Node NoLoopNode(Node head1, Node head2) {
Node cur1 = head1;
Node cur2 = head2;
int n = 0;//用一个变量来计算出两条链表结点个数的差值
while(cur1 != null){
cur1 = cur1.next;
n++;
}
while(cur2 != null){
cur2 = cur2.next;
n--;
}
//两条链表的最后一个结点如果不相等,说明,没有公共链表,不相交
if(cur1 != cur2) return null;
//让cur1等于结点数多的链表的头结点
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);
//让结点数多的链表先多走n步,也就是让cur1和cur2到相交结点的距离相等
while(n > 0){
cur1 = cur1.next;
n--;
}
while(cur1 != cur2){
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
//两个链表都有环的情况
private static Node bothLoopNode(Node head1, Node n1, Node head2, Node n2) {
//两个链表相交结点在环外,和无环的情况求交点一样
if(n1 == n2){
Node cur1 = head1;
Node cur2 = head2;
int n = 0;//用一个变量来计算出两条链表结点个数的差值
while(cur1 != n1){
cur1 = cur1.next;
n++;
}
while(cur2 != n1){
cur2 = cur2.next;
n--;
}
//两条链表的最后一个结点如果不相等,说明,没有公共链表,不相交
if(cur1 != cur2) return null;
//让cur1等于结点数多的链表的头结点
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);
//让结点数多的链表先多走n步,也就是让cur1和cur2到相交结点的距离相等
while(n > 0){
cur1 = cur1.next;
n--;
}
while(cur1 != cur2){
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}else {
/*判断两条有环的链表是否相交,只需让一个链表从环的起点开始移动,如果
中间能遇到另一条链表环的起点,就说明两条环相交,返回哪一个点都可以,
如果没有遇见,说明这两条链表不相交
*/
Node cur = n1.next;
while(cur != n1){
if(cur == n2){
return n1;
}
cur = cur.next;
}
return null;
}
}
如有不足之处,欢迎指正,谢谢!