文章目录
补第三节知识点
01 | 猫狗队列
- 猫狗队列 【题目】 宠物、狗和猫的类如下:
public class Pet { private String type; public Pet(String type) { this.type = type; } public String getPetType() { return this.type; } } public class Dog extends Pet { public Dog() { super("dog"); } } public class Cat extends Pet { public Cat() { super("cat"); } }
- 实现一种狗猫队列的结构,要求如下:
- 用户可以调用add方法将cat类或dog类的 实例放入队列中;
- 用户可以调用pollAll方法,将队列中所有的实例按照进队列的先后顺序依次弹出;
- 用户可以调用pollDog方法,将队列中dog类的实例按照进队列的先后顺序依次弹出;
- 用户可以调用pollCat方法,将队列中cat类的实例按照进队列的先后顺序依次弹出;
- 用户可以调用isEmpty方法,检查队列中是 否还有dog或cat的实例;
- 用户可以调用isDogEmpty方法,检查队列中是否有dog 类的实例;
- 用户可以调用isCatEmpty方法,检查队列中是否有cat类的实例。
- 题目分析:
- 因为题目要求的有猫狗两种类型,因此我们可以建两个队列,dogQ与catQ,分别用于存放猫与狗,这样可以解决用户明确提出要弹出猫还是狗。
- 那么问题来了,当用户使用pollAll()方法,要求不管猫狗,只要弹出最早进队列的那个,那么久无法满足要求了。因此我们还需添加一个标记,例如时间戳,每个进队列的宠物都需要附带时间戳,下列代码中采用了计数器来标记。当pollall()时,先比较两者的count值,小的即为先入队列者。
- 因为题目给了基础代码,最好不要改动基础代码,因此我们编写了PetEnterQueue类以便添加count计数。
- 代码实现:
package class03; import java.util.LinkedList; import java.util.Queue; public class Code_04_DogCatQueue { public static class Pet { private String type; public Pet(String type) { this.type = type; } public String getPetType() { return this.type; } } public static class Dog extends Pet { public Dog() { super("dog"); } } public static class Cat extends Pet { public Cat() { super("cat"); } } public static class PetEnterQueue { private Pet pet; private long count; public PetEnterQueue(Pet pet, long count) { this.pet = pet; this.count = count; } public Pet getPet() { return this.pet; } public long getCount() { return this.count; } public String getEnterPetType() { return this.pet.getPetType(); } } public static class DogCatQueue { private Queue<PetEnterQueue> dogQ; private Queue<PetEnterQueue> catQ; private long count; public DogCatQueue() { this.dogQ = new LinkedList<PetEnterQueue>(); this.catQ = new LinkedList<PetEnterQueue>(); this.count = 0; } public void add(Pet pet) { if (pet.getPetType().equals("dog")) { this.dogQ.add(new PetEnterQueue(pet, this.count++)); } else if (pet.getPetType().equals("cat")) { this.catQ.add(new PetEnterQueue(pet, this.count++)); } else { throw new RuntimeException("err, not dog or cat"); } } public Pet pollAll() { if (!this.dogQ.isEmpty() && !this.catQ.isEmpty()) { if (this.dogQ.peek().getCount() < this.catQ.peek().getCount()) { // 如果狗队列的第一个元素顺序在猫队列第一个元素之前,则弹出狗队列第一个元素 return this.dogQ.poll().getPet(); } else { return this.catQ.poll().getPet(); } } else if (!this.dogQ.isEmpty()) { return this.dogQ.poll().getPet(); } else if (!this.catQ.isEmpty()) { return this.catQ.poll().getPet(); } else { throw new RuntimeException("err, queue is empty!"); } } public Dog pollDog() { if (!this.isDogQueueEmpty()) { return (Dog) this.dogQ.poll().getPet(); } else { throw new RuntimeException("Dog queue is empty!"); } } public Cat pollCat() { if (!this.isCatQueueEmpty()) { return (Cat) this.catQ.poll().getPet(); } else throw new RuntimeException("Cat queue is empty!"); } public boolean isEmpty() { return this.dogQ.isEmpty() && this.catQ.isEmpty(); } public boolean isDogQueueEmpty() { return this.dogQ.isEmpty(); } public boolean isCatQueueEmpty() { return this.catQ.isEmpty(); } } public static void main(String[] args) { DogCatQueue test = new DogCatQueue(); Pet dog1 = new Dog(); Pet cat1 = new Cat(); Pet dog2 = new Dog(); Pet cat2 = new Cat(); Pet dog3 = new Dog(); Pet cat3 = new Cat(); test.add(dog1); test.add(cat1); test.add(dog2); test.add(cat2); test.add(dog3); test.add(cat3); test.add(dog1); test.add(cat1); test.add(dog2); test.add(cat2); test.add(dog3); test.add(cat3); test.add(dog1); test.add(cat1); test.add(dog2); test.add(cat2); test.add(dog3); test.add(cat3); while (!test.isDogQueueEmpty()) { System.out.println(test.pollDog().getPetType()); } while (!test.isEmpty()) { System.out.println(test.pollAll().getPetType()); } } }
02 | 顺时针打印矩阵
- 给定一个整型矩阵matrix,要求:额外空间复杂度为O(1)。
- 解题思路:每次都从左上角开始打印,外圈打印完之后开始向内圈层层缩进。
- 代码实现:
public class PrintMatrix { public void printMatrix(int[][] matrix){ int lr = 0; int lc = 0; int rr = matrix.length - 1; int rc = matrix[0].length - 1; // 左上角的横纵坐标有一个大于等于右下角的横纵坐标的时候就停止打印 while(lr <= rr && lc <= rc){ printEdge(matrix, lr++, lc++, rr--, rc--); } } /** * 打印四条边:边界处理+四个while循环 * @param matrix:矩阵 * @param lr:左上角元素横坐标 * @param lc:左上角元素纵坐标 * @param rr:右下角元素横坐标 * @param rc:右下角元素纵坐标 */ public void printEdge(int[][] matrix, int lr, int lc, int rr, int rc){ if(lr == rr){ // 如果 lr == rr :说明只有一行数据,那只打印这一行数据就可以了 for(int i = lc; i <= rc; i++){ System.out.print(matrix[lr][i] + " "); } }else if(lc == rc){ // 如果 lc == rc :说明只有一列数据,那只打印这一列数据就可以了 for(int i = lr; i <= rr; i++){ System.out.print(matrix[lc][i] + " "); } }else{ int curC = lc; int curR = lr; while (curC != rc){ // 打印上横线 System.out.print(matrix[lr][curC] + " "); curC++; } while (curR != rr){ // 打印右竖线 System.out.print(matrix[curR][rc] + " "); curR++; } while(curC != lc){ // 打印下横线 System.out.print(matrix[rr][curC] + " "); curC--; } while(curR != lr){ // 打印左竖线 System.out.print(matrix[curR][lc] + " "); curR--; } } } // 测试 public static void main(String[] args){ PrintMatrix pm = new PrintMatrix(); int[][] matrix = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 }, { 13, 14, 15, 16 } }; pm.printMatrix(matrix); }
03 | 旋转正方形矩阵
- 给定一个整型正方形矩阵matrix,请把该矩阵调整成顺时针旋转90度的样子。 要求: 额外空间复杂度为O(1)。
- 解题思路:通过一个点找出另外的三个点,并旋转,注意边界条件。
- 如13位置换到1位置、1位置换到4位置、4位置换到16位置、16位置换到13位置;
- 先旋转外圈,再层层递进
- 代码实现:
public class RotateMatrix { /** * 将一个正方形旋转90度 * @param matrix */ public int[][] rotateMatrix(int[][] matrix){ if(matrix.length != matrix[0].length){ throw new IllegalArgumentException("error : the input is error!"); } int lr = 0; int lc = 0; int rr = matrix.length - 1; int rc = matrix[0].length - 1; while(lr < rr){ rotateEdge(matrix, lr++, lc++, rr--, rc--); } return matrix; } /** * 一个正方形圈旋转90度 * @param matrix:正方形 * @param lr:左上角行号 * @param lc:左上角列号 * @param rr:右下角行号 * @param rc:右下角列号 */ public void rotateEdge(int[][] matrix, int lr, int lc, int rr, int rc){ int times = rc - lc; // 旋转的次数 = 行数/列数 - 1 int tmp = 0; // 互换四个点的位置,逆时针依次互换过来 for(int i = 0; i != times; i++){ tmp = matrix[lr][lc + i]; matrix[lr][lc + i] = matrix[rr - i][lc]; // 上横线的位置元素换成左竖线的位置元素 matrix[rr - i][lc] = matrix[rr][rc - i]; // 左竖线的位置元素换成下横线的位置元素 matrix[rr][rc - i] = matrix[lr + i][rc]; // 下横线的位置元素换成右竖线的位置元素 matrix[lr + i][rc] = tmp; // 右竖线的位置元素换成上横线的位置元素 } } // 测试 public static void main(String[] args){ RotateMatrix rm = new RotateMatrix(); int[][] matrix = new int[][]{{1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,16}}; int[][] res = rm.rotateMatrix(matrix); for(int i = 0; i < res.length; i++){ for(int j = 0; j < res[0].length; j++){ System.out.print(res[i][j] + " "); } System.out.println(); } } }
04 | "之"字形打印矩阵
- 给定一个矩阵matrix,按照“之”字形的方式打印这 个矩阵,要求:额外空间复杂度为O(1)。
- 解题思路:
- 使用双指针分别代表行和列
- 看成不断打印斜线,每次打印了一条斜线就换方向打印下一条
- A指针先右再下,B指针先下再右。
- 代码实现:
public class PrintZigMatrix { public void printZigMatrix(int[][] matrix){ // 每次打印轨迹的两个端点:A(a,b) B(c,d) int a = 0, b = 0, c = 0, d = 0; // 矩阵右下角的坐标:(endRow,endCol) int endRow = matrix.length - 1; int endCol = matrix[0].length - 1; // 判断每次打印的方向,每打印一次就需要换向 boolean direction = true; // A是先往右走,走到最右往下走;B是先往下走,走到最下开始往右走 while (a <= endRow && b <= endCol){ printSlash(matrix, a, b, c, d, direction); direction = !direction; // 换向 // A如果走到了最右边,则开始向下走,b必须要在a的下面,因为b值改变了会直接影响到a a = b >= endCol ? a + 1 : a; b = b < endCol ? b + 1 : b; // B如果走到了最下面,则开始向右走,c必须要在d的下面,因为c值改变了会直接影响到d d = c >= endRow ? d + 1 : d; c = c < endRow ? c + 1 : c; } } // 打印A和B两个点之间的轨迹 public void printSlash(int[][] matrix, int a, int b, int c, int d, boolean direction){ // direction为true时为从下往上打印,即 B --> A if(direction){ for(; a <= c; c--, d++){ System.out.print(matrix[c][d] + " "); } }else{ for(; a <= c; a++, b--){ System.out.print(matrix[a][b] + " "); } } } // 测试 public static void main(String[] args){ PrintZigMatrix pzm = new PrintZigMatrix(); int[][] matrix = new int[][]{{1,2,3,4},{5,6,7,8},{9,10,11,12}}; pzm.printZigMatrix(matrix); // 1 2 5 9 6 3 4 7 10 11 8 12 }
05 | 在行列都排好序的矩阵中找数
- 给定一个有N*M的整型矩阵matrix和一个整数K, matrix的每一行和每一 列都是排好序的。实现一个函数,判断K 是否在matrix中。 例如:
0 1 2 5 2 3 4 7 4 4 4 8 5 7 7 9 如果K为7,返回true;如果K为6,返 回false。 【要求】 时间复杂度为O(N+M),额外空间复杂度为O(1)。
- 解题思路:
- 因为每一行每一列都是排好序的,所以我们每次从右上角开始查找,如果比右上角值小则往左移动,否则往下移动。
- 找到则返回true,否则返回false
- 代码实现:
public class Code_09_FindNumInSortedMatrix { public static boolean isContains(int[][] matrix, int K) { int row = 0; int col = matrix[0].length - 1; while (row < matrix.length && col > -1) { if (matrix[row][col] == K) { return true; } else if (matrix[row][col] > K) { col--; } else { row++; } } return false; } public static void main(String[] args) { int[][] matrix = new int[][] { { 0, 1, 2, 3, 4, 5, 6 },// 0 { 10, 12, 13, 15, 16, 17, 18 },// 1 { 23, 24, 25, 26, 27, 28, 29 },// 2 { 44, 45, 46, 47, 48, 49, 50 },// 3 { 65, 66, 67, 68, 69, 70, 71 },// 4 { 96, 97, 98, 99, 100, 111, 122 },// 5 { 166, 176, 186, 187, 190, 195, 200 },// 6 { 233, 243, 321, 341, 356, 370, 380 } // 7 }; int K = 233; System.out.println(isContains(matrix, K)); } }
06 | 反转单向和双向链表
- 分别实现反转单向链表和反转双向链表的函数。 【要求】 如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)
- 反转单向链表解题思路:
- 从头到尾一个结点一个结点的挨个处理:将当前结点(head) 和下一个结点断开,指向前一个结点。
- 例如,a->b->c->d,当把b的指针指向a时,即 a<-b c->d,能看到b、c指点链表断开了,因此,我们在调整链表的时候需要预存b的下一个,以防链表断开。
- 因此我们需要三个指针,分别为指向当前节点、前一个结点、后一个结点。
- 代码实现:
public class ReverseLinkedList { public static class Node{ public int value; public Node next; public Node(int value){ this.value = value; } } public static Node reverseLinkedList(Node head){ if(head == null){ return null; } Node pre = null; // 当前节点的前一个节点 Node next = null; // 当前节点的后一个节点 while(head != null){ // 先用next保存head的下一个节点的信息 // 保证单链表不会因为失去head节点的原next节点而就此断裂 next = head.next; // 保存完next,就可以让head从指向next变成指向pre了 head.next = pre; // head指向pre后,就继续依次反转下一个节点 // 让pre、head、next依次向后移动一个节点,继续下一个节点的指针反转 pre = head; head = next; } // 如果head为null的时候,pre就为最后一个节点了,此时链表已经反转完毕,pre就是反转后链表的第一个节点 return pre; }
- 反转双向链表 解题思路:从头到尾一个结点一个结点的挨个处理:对每个结点,交换其next和pre即可,并记录当前的引用(仅仅为了最后的返回)。
- 代码实现:
public class ReverseDoubleLinkedLsit { public static class DoubleNode{ int val; DoubleNode pre; // 指向前一个节点 DoubleNode next; // 指向后一个节点 public DoubleNode(int value){ this.val = value; } } public static DoubleNode reverseDoubleLinkedList(DoubleNode head){ if(head == null){ return null; } DoubleNode tmp = null; DoubleNode res = null; // res的作用仅仅是记录head,因为最后一次循环后head为null,但是我们要返回最后一个不为null的head // 从头节点开始,依次往后挨个处理 while(head != null){ // pre和next指针互换 tmp = head.next; head.next = head.pre; head.pre = tmp; res = head; // 记录head节点 head = tmp; // 往后推进一个节点 } return res; }
07 | 打印两个有序链表的公共部分
- 给定两个有序链表的头指针head1和head2,打印两个 链表的公共部分。
- 实现代码:
public class Code_10_PrintCommonPart { public static class Node { public int value; public Node next; public Node(int data) { this.value = data; } } public static void printCommonPart(Node head1, Node head2) { System.out.print("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.print(head1.value + " "); head1 = head1.next; head2 = head2.next; } } System.out.println(); } public static void printLinkedList(Node node) { System.out.print("Linked List: "); while (node != null) { System.out.print(node.value + " "); node = node.next; } System.out.println(); } public static void main(String[] args) { Node node1 = new Node(2); node1.next = new Node(3); node1.next.next = new Node(5); node1.next.next.next = new Node(6); Node node2 = new Node(1); node2.next = new Node(2); node2.next.next = new Node(5); node2.next.next.next = new Node(7); node2.next.next.next.next = new Node(8); printLinkedList(node1); printLinkedList(node2); printCommonPart(node1, node2); } }
08 | 判断一个链表是否为回文结构
-
给定一个链表的头节点head,请判断该链表是否为回文结构。 例如: 1->2->1,返回true。 1->2->2->1,返回true。 15->6->15,返回true。 1->2->3,返回false。
-
进阶: 如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1)。
-
解题思路:
- 非进阶版:isPalindrome1,借用栈。遍历一次链表,将每个结点放入栈中,那么,栈中弹出的则是链表的逆序,这是只需一个个比对即可,若有不用的直接返回false,否则返回true。所用空间复杂度:O(N)
- 对isPalindrome1进行改进,同样需要借用栈与快慢指针,快指针一次走两步,慢指针一次走一步。当快指针结束遍历时,慢指针刚好到中点,这时将慢指针遍历的后半部分压栈,然后依次从栈中弹出与链表的前半部分进行比较。空间复杂度减半:O(N/2)
- 进阶版:isPalindrome2,先找到链表的中点,再把后半部分的链表反转,中间的结点指向null,然后前后部分进行比较,一样则是回文。找中点可使用2中用到的快慢指针,(tip:若结点为奇数,慢指针会指向正中位置;若为偶数,慢指针会指向两个中点中的前一个)。注意!判断完成后要将链表的部分反转回去,不能把题目的结构给改了。
-
代码实现:
public class IsPalindromList { public static class Node{ int val; Node next; public Node(int val){ this.val = val; } } // 方法1:用栈结构存储整个链表元素,再依次弹出与链表从头开始比较,空间复杂度为:O(n) public boolean isPalindromList1(Node head){ if(head == null || head.next == null){ return true; // 没有节点或者只有一个节点时,肯定是回文链表 } Node cur = head; Stack<Node> stack = new Stack<Node>(); // 遍历链表,将链表元素从头开始依次亚入栈中 while(cur != null){ stack.push(cur); cur = cur.next; } // 开始比对栈中依次弹出的元素与链表从头开始遍历的元素是否都相等 cur = head; while(cur != null){ if(stack.pop() != cur){ return false; } cur = cur.next; } return true; } // 方法2:用栈只存储链表一半的元素(中间位置到最后),然后依次弹出与链表的前半部分比较 // 空间复杂度为:O(n/2) // 代码略 // 方法3:将链表对折,后半部分的链表反转与前半部分链表进行比较 public boolean isPalindromList2(Node head){ if(head == null || head.next == null){ return true; // 没有节点或者只有一个节点时,肯定是回文链表 } Node cur = head; Node slow = head; // 慢指针:一次走一个节点 Node fast = head; // 快指针:一次走两个节点 // 元素总个数为奇数时,慢指针最后指向中间位置,若为偶数,则走到中间位置的前一位 // 注意:在向后遍历的时候,需要判断快指针指向的节点是否为空,不然会出现异常 while(fast.next != null && fast.next.next != null){ fast = fast.next.next; // 若fast.next != null,那么说明这是偶数个 slow = slow.next; } // slow 到达的是中点位置,反转后半部分,反转后中点指向的是null Node end = reverseSingleList(slow); fast = end; // 将前半部分与后半部分折叠对比 while(cur != null && fast != null){ if(cur.val != fast.val){ return false; } cur = cur.next; fast = fast.next; } // 不能改变原有的数据结构,所以还需要将后半部分反转还原过去 cur = reverseSingleList(end); return true; } // 链表反转 public Node reverseSingleList(Node head){ Node pre = null; Node next = null; while(head != null){ next = head.next; head.next = pre; // 往后推进一个节点 pre = head; head = next; } return pre; } }
09 | 将单向链表按某值划分成左边小、中间相等、右边大的形式
- 给定一个单向链表的头节点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的节点即可。对某部分内部的节点顺序不做 要求。
- 进阶: 在原问题的要求之上再增加如下两个要求。 在左、中、右三个部分的内部也做顺序要求,要求每部分里的节点从左 到右的 顺序与原链表中节点的先后次序一致。 例如:链表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)。
- 解题思路:
- 基础版:listPartition1,需要用到额外的空间(数组),先遍历一遍链表,将数据存放在数组中,然后回到荷兰国旗(不稳定的)问题,再将数组中的数据转移到链表中。
- 进阶版:listPartition2,需稳定且空间复杂度为O(1),用六个变量将原链表拆分成三个小链表,最后将其首尾相连即可。遍历链表使less等于第一个小于num的结点,equal等于第一个等于num的结点,more指向第一个大于num的结点,即less、equal、more都是结点类型。用endless、endequal、endmore来添加结点,属于哪一个就加在哪一个的后面,这样可以保证稳定性。
public class LinkedPartition {
public static class Node{
int val;
Node next;
public Node(int val){
this.val = val;
}
}
// 将链表中的元素先放进数组中,然后再进行划分
public Node listPartition1(Node head, int num){
if(head == null || head.next == null){
return head;
}
int i = 0;
Node cur = head;
// 计算有多少个节点
while(cur != null){
i++;
cur = cur.next;
}
int[] arr = new int[i]; // 申请一个和链表中元素个数相等的数组
cur = head;
i = 0;
// 从链表头结点开始遍历,将节点的val存进数组中
while(cur != null){
arr[i++] = cur.val;
cur = cur.next;
}
// 在数组中使用荷兰国旗的方法对值进行小、等于、大的区域划分
arrPartition(arr, num);
// 按照排好序的数组顺序,将对应val节点串起来
cur = head;
i = 0;
while(cur != null){
cur.val = arr[i++];
cur = cur.next;
}
return head;
}
public void arrPartition(int[] arr, int num){
int less = -1;
int more = arr.length;
int cur = 0;
while(cur != more){
if(arr[cur] < num){
// 当前数与最小区域边界后一个位置元素进行交换,cur指针推进一位
swap(arr, ++less, cur++);
}else if(arr[cur] > num){
// 当前数与最大区域边界的前一个位置元素进行交换,cur指针不变
swap(arr, cur, --more);
}else{
cur++; // 等于时,cur自增
}
}
}
public void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
===============================
public Node listPartition2(Node head, int num){
Node less = null; // 存放小于num的节点,指向第一次出现在该区域的节点
Node equal = null;
Node more = null;
Node endless = null;
Node endequal = null; // 指向各自链表的结尾
Node endmore = null;
Node next = null;
while(head != null){
next = head.next;
head.next = null; // 每次加入的节点都是指向null,就是less、equal、more的末尾节点
if(head.val < num) {
// 放入less区域
if (less == null) {
less = head;
endless = head;
} else {
endless.next = head; // less区域的尾节点指针指向head
endless = head; // 推进链表,将endless指针指向head节点
}
}else if(head.val > num){
// 放入more区域
if(more == null){
more = head;
endmore = head;
}else{
endmore.next = head;
endmore = head;
}
}else{
if(equal == null){
equal = head;
endequal = head;
}else{
endequal.next = head;
endequal = head;
}
}
head = head.next;
}
// 将划分好的三部分子链表串起来,返回
// 需要考虑到可能某部分子链表可能不存在的情况
if(less != null){
endless.next = equal; // less子链表存在
if(equal != null){
endequal.next = more; // equal子链表存在
}else{
endless.next = more; // equal子链表不存在
}
return less;
}else{
// less子链表不存在
if(equal != null){
endequal.next = more;
return equal;
}else{
// equal子链表不存在,直接返回more子链表
return more;
}
}
}
10 | 复制含有随机指针结点的链表
-
一种特殊的链表节点类描述如下:
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,请实现一个 函数完成 这个链表中所有结构的复制,并返回复制的新链表的头节点。
-
进阶: 不使用额外的数据结构,只用有限几个变量,且在时间复杂度为 O(N) 内完成原问题要实现的函数。
-
解题思路:
- 非进阶版: copyListWithRand1 利用hashmap 实现原链表结点和复制结点的映射。而想复制链表结点之间对应关系可由查找原结点之间的关系得到。在map中,key为原结点,value为复制的结点。如:A’需要连接到B‘,可由A找到B,在获取B的value即可连接到B’。
- 进阶版:copyListWithRand2 不使用额外空间。首先,先将结点复制到链表中,形成:1->1’->2->2’->3->3’->…->null形式;其次复制rand结构,如1‘的rand指针需指向3’,则又1指针指向3, 3的next为3‘,因此可得出1’的rand指向3‘。最后将链表拆分得到复制链表。
-
代码实现:
public class CopyListWithRandom { public static class Node{ int value; Node next; Node random; // 指向链表中任一节点或者null public Node(int value){ this.value = value; } } // 利用hashmap来进行元素列表节点和复制节点之间的映射,key存原节点,value存对应的复制节点 public Node copyListWithRandom1(Node head){ // hashmap的key和value存的都是Node类型 HashMap<Node,Node> map = new HashMap<Node, Node>(); Node cur = head; // 第一次遍历:拷贝节点,形成节点之间的映射关系 while(cur != null){ map.put(cur, new Node(cur.value)); cur = cur.next; } cur = head; // 第二次遍历:复制节点之间的关系,即:next和random指针 // 原链表节点之间的指针关系是知道的,比如想知道A'和B'之间的关系,可以通过:A->B->B',这样就找到了B' while(cur != null){ // key为cur的value节点的next指针指向的是key为cur.next对应的value节点 map.get(cur).next = map.get(cur.next); // key为cur的value节点的random指针指向的是key为cur.random对应的value节点 map.get(cur).random = map.get(cur.random); cur = cur.next; } return map.get(head); // 返回复制链表的头结点 } ================================== public Node copyListWithRandom2(Node head){ if(head == null){ return null; } Node cur = head; Node tmp = null; // 拷贝节点,重建链表结构为:1->1'->2->2'->3->3'->...->null形式 // 即将拷贝的节点直接关联到原节点的next指针上 while(cur != null){ tmp = head.next; // 先将当前指针原链表中的下一个节点保存起来 cur.next = new Node(cur.value); // 将当前节点复制的节点设置为当前节点的next节点 cur.next.next = tmp; // 将原节点的next节点设置为员节点拷贝节点的next节点 cur = cur.next.next; } cur = head; Node curCopy = head.next; // 复制random结构 while(cur != null){ curCopy = cur.next; // 拷贝节点的random节点就是cur的random节点的后一个节点 curCopy.random = (cur.random == null) ? null : cur.random.next; cur = cur.next.next; } Node headCopy = head.next; cur = head; // 拆分链表 while(cur != null){ curCopy = cur.next; cur.next = cur.next.next; // 走两个next curCopy.next = curCopy.next == null ? null : curCopy.next.next; cur = cur.next; // 推进节点 } return headCopy; }
11 | 两个单链表相交的一系列问题
-
本题中,单链表可能有环,也可能无环。给定两个单链表的头节点 head1和head2,这两个链表可能相交(相交指的是内存地址相等,而不是值相等),也可能不相交。请实现一个函数, 如果两个链表相交,请返回相交的第一个节点;如果不相交,返回null 即可。
-
进阶:要求:如果链表 1 的长度为N,链表 2 的长度为 M,时间复杂度请达到 O(N+M),额外空间复杂度请达到O(1)。
-
解题思路:这道题揉合了三个问题:
- 两个单链表是否有环,有环则返回入环结点,否则null
- 两个无环单链表是否相交,相交则返回相交的第一个结点,否则null
- 两个有环单链表是否相交,相交则返回相交的第一个结点,否则null
-
两个单链表是否有环问题
- 解法一:使用hashset存储遍历过的节点,每次存储前,都先查询下给节点是否存在,如果存在则存在环;
- 解法二:使用两个指针,快指针一次走两步,慢指针一次走一步。当两个指针相遇时,快指针回到起点一次走一步,慢指针在相遇的节点处继续往下一次走一步,如有环将会在环的入口节点处相遇。
// 1.检查单链表是否有环:利用HashSet去重的特性完成 public Node getLoopNode(Node head){ HashSet<Node> set = new HashSet<>(); while(head != null){ if(!set.contains(head)){ set.add(head); // 若head节点不在set里,则add进去 head = head.next; // 向后推进链表 }else{ // 到这里说明head节点在set里已经存在了,即有环,此节点即为环的入口节点 return head; } } return null; } ====================== //利用快慢指针 public class EntryNodeOfLoop_Method3 { class ListNode{ private int val; private ListNode next = null; public ListNode(int val){ this.val = val; } } public ListNode entryNodeOfInLoop(ListNode head){ if(head == null || head.next == null){ return null; } ListNode p1 = head; ListNode p2 = head; while(p2 != null && p2.next != null){ p1 = p1.next; p2 = p2.next.next; if(p1 == p2){ // p1和p2相遇,证明有环 p2 = head; while(p1 != p2){ p1 = p1.next; p2 = p2.next; } if(p1 == p2){ return p1; } } } return null; } }
-
两无环单链表相交求相交结点问题
- 解法一:**利用hashset。**将head1的数据放入set中,依次遍历head2,看set中是否存在对应数据,第一个存在的就是相交的开始结点。
- 解法二:不使用额外的数据结构。 先遍历head1,统计其长度len1,拿到head1的最后一个结点end1;在遍历head2,统计其长度len2,拿到head1的最后一个结点end2。比较end1与end2是否相等(比较内存地址而不是值),如果不相等则不相交;如果相等说明相交,但最后一个结点未必是第一个相交的结点。这是我们比较链表长度,如len1=100,len2=80,则让head1先走20步,之后两个链表同时走,最终会走到首次相交的地方。
// 2.两无环单链表是否相交,相交则返回相交的第一个节点,不相交返回null public Node noLoop(Node head1, Node head2){ HashSet<Node> set = new HashSet<>(); // 将head1链表放入set中 while(head1 != null){ set.add(head1); head1 = head1.next; } // 遍历head2链表,与set集合中的head1链表的节点进行比较,看是否有相等的 while(head2 != null){ if(set.contains(head2)){ return head2; } head2 = head2.next; } return null; // 遍历完head2都没有与head1有重复的节点,说明不相交 } ==================== //解法二 public static Node noLoop(Node head1, Node head2) { if (head1 == null || head2 == null) { return null; } Node cur1 = head1; Node cur2 = head2; int n = 0; //两链表长度的差值 while (cur1.next != null) { n++; cur1 = cur1.next; } while (cur2.next != null) { n--; cur2 = cur2.next; } if (cur1 != cur2) { return null; } cur1 = n > 0 ? head1 : head2; cur2 = cur1 == head1 ? head2 : head1; n = Math.abs(n); while (n != 0) { n--; cur1 = cur1.next; } while (cur1 != cur2) { cur1 = cur1.next; cur2 = cur2.next; } return cur1; }
-
两个有环单链表相交求相交结点问题
-
可分为三种情况:①两链不相交 ②先相交再有环 ③在环上相交。
-
一个单链表有环,一个单链表无环不可能出现相交的情况。
-
loop1、loop2为两链表开始入环的结点,当loop1==loop2时,属于第二种情况,这时只看上半部分就等同于无环链表相交问题;当loop1!=loop2时,属于第一种或第三种情况。使用loop1在环上遍历,在loop1遍历环一圈转回loop1的时候依旧没找到loop2说明两环不相交,是情况一;而在遍历过程中找到了loop2是情况三,这时返回loop1或者loop2都可以。
-
-
代码实现:
// 3.两有环链表是否相交 public Node bothLoop(Node head1, Node loop1, Node head2, Node loop2){ // 环外相交:可归结为无环单链表找相交点 if(loop1 == loop2){ HashSet<Node> set = new HashSet<>(); while(head1 != loop1){ // 遍历head1环以外的节点 set.add(head1); head1 = head1.next; } while(head2 != loop2){ // 将head2环外的节点与head1环外的节点进行对比,看是否有相等的 if(set.contains(head2)){ return head2; } head2 = head2.next; } return loop1; }else{ // 两个6形式无相交 + 环内相交(不同的环入口点) Node cur = loop1.next; while(cur != loop1){ // cur从loo1开始在环中向下遍历,若直到再次遍历到loop1位置处,也没有遇到loop2,则说明二者不相交 if(cur == loop2){ return loop1; // 遇见了loop2,则说明相交,即为环内相交的情况 } cur = cur.next; } return null; // cur遍历完它自己那个环都没有遇到loop2,则说明不相交,即为两个6的情况 } }
-
代码整合:
public class Code_14_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); 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; } public static Node getLoopNode(Node head) { if (head == null || head.next == null || head.next.next == null) { return null; } Node n1 = head.next; // n1 -> slow Node n2 = head.next.next; // n2 -> fast while (n1 != n2) { if (n2.next == null || n2.next.next == null) { return null; } n2 = n2.next.next; n1 = n1.next; } n2 = head; // n2 -> walk again from head while (n1 != n2) { n1 = n1.next; n2 = n2.next; } return n1; } public static Node noLoop(Node head1, Node head2) { if (head1 == null || head2 == null) { return null; } Node cur1 = head1; Node cur2 = head2; int n = 0; //两链表长度的差值 while (cur1.next != null) { n++; cur1 = cur1.next; } while (cur2.next != null) { n--; cur2 = cur2.next; } if (cur1 != cur2) { return null; } cur1 = n > 0 ? head1 : head2; cur2 = cur1 == head1 ? head2 : head1; n = Math.abs(n); while (n != 0) { n--; 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 n = 0; while (cur1 != loop1) { n++; cur1 = cur1.next; } while (cur2 != loop2) { n--; cur2 = cur2.next; } cur1 = n > 0 ? head1 : head2; cur2 = cur1 == head1 ? head2 : head1; n = Math.abs(n); while (n != 0) { n--; 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; } } public static void main(String[] args) { // 1->2->3->4->5->6->7->null Node head1 = new Node(1); head1.next = new Node(2); head1.next.next = new Node(3); head1.next.next.next = new Node(4); head1.next.next.next.next = new Node(5); head1.next.next.next.next.next = new Node(6); head1.next.next.next.next.next.next = new Node(7); // 0->9->8->6->7->null Node head2 = new Node(0); head2.next = new Node(9); head2.next.next = new Node(8); head2.next.next.next = head1.next.next.next.next.next; // 8->6 System.out.println(getIntersectNode(head1, head2).value); // 1->2->3->4->5->6->7->4... head1 = new Node(1); head1.next = new Node(2); head1.next.next = new Node(3); head1.next.next.next = new Node(4); head1.next.next.next.next = new Node(5); head1.next.next.next.next.next = new Node(6); head1.next.next.next.next.next.next = new Node(7); head1.next.next.next.next.next.next = head1.next.next.next; // 7->4 // 0->9->8->2... head2 = new Node(0); head2.next = new Node(9); head2.next.next = new Node(8); head2.next.next.next = head1.next; // 8->2 System.out.println(getIntersectNode(head1, head2).value); // 0->9->8->6->4->5->6.. head2 = new Node(0); head2.next = new Node(9); head2.next.next = new Node(8); head2.next.next.next = head1.next.next.next.next.next; // 8->6 System.out.println(getIntersectNode(head1, head2).value); } }