题目一 数组结构实现固定的队列和栈
1 栈
新建数组(固定长度)、index指针
index指针指向新加数的位置
基本操作:
1、push压栈:先检查栈是否已满,报异常;否则压栈,index++
2、peek弹出栈顶元素但不删除:检查栈是否已满,弹出的是index - 1(注意index指向的元素含义)
3、 pop弹出栈顶元素并删除:检查栈是否已满,return栈顶元素,–index
代码实现
public class ArrayStack {
public int[] arr;
public int index;
//1 设置一个固定数组大小initSize,初始化数组arr
public ArrayStack(int initSize){
if(initSize < 0){
throw new IllegalArgumentException("The init Size is less than 0");//不合法的参数异常
}
arr = new int[initSize];
index = 0;
}
//2 栈操作:peek\push\pop
public void push(int obj){//压栈
if(index == arr.length){
throw new ArrayIndexOutOfBoundsException("The queue is full");//数组越界
}
obj = arr[index++];
}
public int pop(){//弹栈顶
if(index == 0){
throw new ArrayIndexOutOfBoundsException("The queue is empty");
}
return arr[--index];
}
public Integer peek(){
if(index == 0){
return null;
}
return arr[index - 1];
}
}
2 队列(难度+)
新建数组(固定长度);两个指针:start、end;队列中个数size
两指针start和end之间是独立的,无约束关系,end和start一开始均指向0位置。
end变量代表新加一个数应该把它填到什么位置上,start变量代表用户拿一个数时应该拿哪个数。
size与两指针之间分别有约束关系:
1、size < length:将新数放到end位置上,size++,end++
2、size != 0:将start指的数返回给用户,size–,start++
3、end/start只要到底了,就回到开头,start在不断的追end,是一个循环
基本操作: peek、push、poll(同上)
代码实现
public class ArrayQueue {
//1 初始化
public int[] arr;
public int size;
public int start;
public int end;
public ArrayQueue(int initSize){
if(initSize < 0){
throw new IllegalArgumentException("The init size is less than 0");
}
arr = new int[initSize];
size = 0;
start = 0;
end = 0;
}
//2 基本操作
public Integer peek(){
if(size == 0){
return null;
}
return arr[start];
}
public void push(int obj){
if(size == arr.length){
throw new ArrayIndexOutOfBoundsException("The queue is full");
}
size++;
obj = arr[end];
end = end == arr.length - 1 ? 0 : end + 1;//如果end指针到底了就返回头部0位置,否则 + 1
}
public int poll(){
if(size == 0){
throw new ArrayIndexOutOfBoundsException("The queue is empty");
}
size--;
int temp = start;
start = start == arr.length - 1 ? 0 : start + 1;
return arr[temp];
}
题目二 实现特殊的栈,并返回栈中最小元素(比较著名的题)
实现一个特殊的栈,在实现栈的基本功能的基础上,再实现返回栈中最小元素的操作。
要求:1、pop、push、getMin操作的时间复杂度O(1);2、设计的栈类型可以使用现成的栈结构
思路
使用两个栈结构data栈和min栈,两个栈元素一起增长,分别存放数据和当前最小值。设要压入元素num,若num < data栈栈顶,则两个栈同时压入num;若num > data栈栈顶,则data栈压入num,min栈压入min栈的最小值(重复压入),则需要返回时去min栈栈顶即可。
增加空间,维持最小值。
代码实现
public static class MyStack2{
private Stack<Integer> stackData;
private Stack<Integer> stackMin;
public MyStack2(){
this.stackData = new Stack<Integer>();
this.stackMin = new Stack<Integer>();
}
//基本操作
//压栈
public void push(int newNum){
if(this.stackMin.isEmpty()){
this.stackMin.push(newNum);
}else if(newNum < this.getmin()){
this.stackMin.push(newNum);
}else {
int newMin = this.stackMin.peek();
this.stackMin.push(newMin);
}
this.stackData.push(newNum);
}
//弹出
public int pop(){
if(this.stackMin.isEmpty()){
throw new RuntimeException("The stack is empty");//该异常在下面详细阐述
}
this.stackMin.pop();
return this.stackData.pop();
}
//获取stackMin栈中栈顶元素
public int getmin(){
if(this.stackMin.isEmpty()){
throw new RuntimeException("The stack is empty");
}
return this.stackMin.peek();
}
}
补充知识点
Exception 和 RuntimeException的区别:
1、其他Exception,受检查异常。
可以理解为错误,必须要开发者解决以后才能编译通过,解决的方法有两种:1.throw到上层 2.try-catch处理。
2、RunTimeException:运行时异常,又称不受检查异常,不受检查!
因为不受检查,所以在代码中可能会有RunTimeException时Java编译检查时不会告诉你有这个异常,但是在实际运行代码时则会暴露出来,比如经典的1/0,空指针等。如果不处理也会被Java自己处理。
题目三 如何用队列实现栈,用栈实现队列
适用范围举例:面试官提问使用队列结构来进行图的深度优先遍历?将队列转化为栈进行遍历
1 用队列实现栈
思路
用两个队列实现栈,两个队列data和help,data中装栈读入的数据,help中装不弹出的数据。
举个简单例子,栈中装入数据[1,2,3,4,5],根据先入后出,应弹出5,则data队列中装入[1,2,3,4,5],help队列从data队列弹出并装入[1,2,3,4],留最后一个数返回,data队列弹出数据,并将data和help队列互换。
代码实现
public static class TwoQueuesStack{
private Queue<Integer> data;
private Queue<Integer> help;
public TwoQueuesStack(){
data = new LinkedList<Integer>();//LinkedList双向链表
help = new LinkedList<Integer>();
}
//入栈
public void push(int pushInt){
data.add(pushInt);
}
//读栈顶
public int peek(){
if(data.isEmpty()){
throw new RuntimeException("Stack is empty");
}
while(data.size() > 1){
help.add(data.poll());//pop的安全方法
}
int res = data.poll();
help.add(res);//因为是peek,不改变栈内数据
swap();
return res;
}
//弹栈顶并删除
public int pop(){
if(data.isEmpty()){
throw new RuntimeException("Stack is empty");
}
while(data.size() > 1){
help.add(data.poll());
}
int res = data.poll();
swap();
return 0;
}
public void swap(){
Queue<Integer> tmp = help;
help = data;
data = tmp;
}
}
2 用栈实现队列
思路
用两个栈实现队列,初始化两个栈:push栈和pop栈。push栈压入队列中的数,pop栈用来存放push栈中pop出来的数。弹出pop栈中的元素,即队列顺序。
实现过程中必须要满足的两个原则:
1、push栈只要倒出,必须一次性倒完,否则pop栈从弹出的第一个元素开始顺序错误
2、pop栈中有数据时,push栈中不可以进行倒出操作,否则pop栈顺序错误
代码实现
public static class TwoStacksQueue{
private Stack<Integer> stackPush;
private Stack<Integer> stackPop;
public TwoStacksQueue(){
stackPush = new Stack<Integer>();
stackPop = new Stack<Integer>();
}
//入队列
public void push(int pushInt){
stackPush.push(pushInt);
}
//出队列
public int poll(){
if(stackPush.isEmpty()&&stackPop.isEmpty()){
throw new RuntimeException("Queue is empty");
}else if(stackPop.isEmpty()){
while (!stackPush.isEmpty()){
stackPop.push(stackPush.pop());
}
}
return stackPop.pop();
}
//出队列,不删除
public int peek(){
if(stackPush.isEmpty() && stackPop.isEmpty()){
throw new RuntimeException("Stack is empty");
}else if(stackPop.isEmpty()){
stackPop.push(stackPush.pop());
}
return stackPop.peek();
}
}
题目四 猫狗队列
宠物、 狗和猫的类如下:
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");
}
}
实现一种狗猫队列的结构, 要求如下: 用户可以调用add方法将cat类或dog类的实例放入队列中; 用户可以调用pollAll方法, 将队列中所有的实例按照进队列的先后顺序依次弹出; 用户可以调用pollDog方法, 将队列中dog类的实例按照进队列的先后顺序依次弹出; 用户可以调用pollCat方法, 将队列中cat类的实例按照进队列的先后顺序依次弹出; 用户可以调用isEmpty方法, 检查队列中是否还有dog或cat的实例; 用户可以调用isDogEmpty方法, 检查队列中是否有dog类的实例; 用户可以调用isCatEmpty方法, 检查队列中是否有cat类的实例。
思路
可以给每个入队列的元素加一个时间戳count,根据count大小弹出队列
代码实现
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> dogQueue;
private Queue<PetEnterQueue> catQueue;
private long count;
public DogCatQueue(){
this.catQueue = new LinkedList<PetEnterQueue>();
this.dogQueue = new LinkedList<PetEnterQueue>();
this.count = 0;
}
public void add(Pet pet){
if(pet.getPetType().equals("dog")){
this.dogQueue.add(new PetEnterQueue(pet, this.count++));
}else if(pet.getPetType().equals("cat")){
this.catQueue.add(new PetEnterQueue(pet, this.count++));
}else{
throw new RuntimeException("not a cat or a dog");
}
}
public Pet pollAll(){
if(!this.dogQueue.isEmpty() && !this.catQueue.isEmpty()){
if (this.dogQueue.peek().getCount() < this.catQueue.peek().getCount()){
return this.dogQueue.poll().getPet();
}else{
return this.catQueue.poll().getPet();
}
}else if(!this.dogQueue.isEmpty()){
return this.dogQueue.poll().getPet();
}else if(!this.catQueue.isEmpty()){
return this.catQueue.poll().getPet();
}else{
throw new RuntimeException("queue is empty");
}
}
public Dog pollDog(){
if(!this.dogQueue.isEmpty()){
return (Dog)this.dogQueue.poll().getPet();
}else{
throw new RuntimeException("dog queue is empty");
}
}
public Cat pollCat(){
if(!this.catQueue.isEmpty()){
return (Cat)this.catQueue.poll().getPet();
}else{
throw new RuntimeException("cat queue is empty");
}
}
public boolean isEmpty(){
return this.dogQueue.isEmpty() && this.catQueue.isEmpty();
}
public boolean isDogEmpty(){
return this.dogQueue.isEmpty();
}
public boolean isCatEmpty(){
return this.catQueue.isEmpty();
}
}
题目五 转圈打印矩阵
给定一个整型矩阵matrix, 请按照转圈的方式打印它。
例如:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
打印结果为: 1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9,
5, 6, 7, 11, 10
要求额外空间复杂度为O(1)
思路
将题目简化思考。
1、首先考虑这样的问题:给了二维数组左上角和右下角的点,可以打印出外框(四条边),即二维数组的子矩阵。这种情况只需要处理两个边界:两个点是否在同一行/同一列?
已知矩阵两个点(tR, tC),(dR, dC),打印出边框(矩阵未必是正方形的)。
2、打印完外面一圈之后,最外面一圈最后一个数正好是第二圈的第一个数,所以打印第二圈时只需要将两个点的坐标修改一下,继续打印即可,两个坐标错位即结束。
图中红色是第一圈,绿色是第二圈,以此类推。(灵魂画家,梵高转世)
代码实现
public static void sprialOrderPrint(int[][] matrix){
int tR = 0;
int tC = 0;
int dR = matrix.length - 1;
int dC = matrix[0].length - 1;
while(tR <= dR && tC <= dC){
printEdge(matrix, tR++, tC++, dR--, dC--);
}
}
public static void printEdge(int[][]m, int tR, int tC, int dR, int dC){
//边界条件:同行或同列
if(tR == dR){
for(int i = tC; i <= dC; i++){
System.out.print(m[tR][i] + " ");
}
}else if(tC == dC){
for(int i = tR; i <= dR; i++){
System.out.print(m[i][tC] + " ");
}
}else{
int curC = tC;
int curR = tR;
//第一行
while(curC != dC){
System.out.print(m[tR][curC] + " ");
curC++;
}
//右侧边
while(curR != dR){
System.out.print(m[curR][dC] + " ");
curR++;
}
//最后一行
while(curC != tC){
System.out.print(m[dR][curC] + " ");
curC--;
}
//左侧一列
while(curR != tR){
System.out.print(m[curR][tC] + " ");
curR--;
}
}
}
题目六 旋转正方形矩阵
给定一个整型正方形矩阵matrix, 请把该矩阵调整成顺时针旋转90度的样子。要求:额外空间复杂度为O(1)。
(面试官并不是sb,不会让你用辅助数组,快醒醒)
思路
顺时针旋转90度后,如下图所示。数字1将会旋转到数字4位置,数字4将会转到数字16位置……依次类推,将这一圈数字一个一个旋转,而并非一排一排旋转,化大为小。
代码实现
public static void rotate(int[][] matrix){
int tR = 0;
int tC = 0;
int dR = matrix.length - 1;
int dC = matrix[0].length - 1;
while(tR < dR){
rotateEdge(matrix, tR++, tC++, dR--, dC--);
}
}
public static void rotateEdge(int[][] m, int tR, int tC, int dR, int dC){
int times = dC - tC;
int temp = 0;
for(int i = 0; i <= times; i++){
//注意替换时的顺序,容易搞反
temp = m[tR][tC + i];
m[tR][tC + i] = m[dR - i][tC];
m[dR - i][tC] = m[dR][dC - i];
m[dR][dC - i] = m[tR + i][dC];
m[tR + i][dC] = temp;
}
}
public static void printMatrix(int[][] matrix){
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[0].length; j++) {
System.out.print(matrix[i][j] + " ");
}
}
题目七 反转单向和双向链表
分别实现反转单向链表和反转双向链表的函数。
要求:如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1)
单向列表(双指针)
public static Node reverseList(Node head){
Node pre = null;
Node next = null;
while(head != null){
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
双向链表
public static DoubleNode reverseList(DoubleNode head){
DoubleNode pre = null;
DoubleNode next = null;
while(head != null){
next = head.next;
head.next = pre;
head.last = next;
pre = head;
head = next;
}
return pre;
}
题目八 “之” 字形打印矩阵
给定一个矩阵matrix, 按照“之” 字形的方式打印这个矩阵, 例如:
1 2 3 4
5 6 7 8
9 10 11 12
“之” 字形打印的结果为: 1, 2, 5, 9, 6, 3, 4, 7, 10, 11,8, 12
要求: 额外空间复杂度为O(1)。
思路
使用两个指针A、B,最开始同时指向矩阵的[0][0]位置,A指针向右走,走到头时向下走,B指针向下走,走到头时向右走。
代码实现
public static void printMatrix(int[][] matrix){
int tR = 0;
int tC = 0;
int dR = 0;
int dC = 0;
int endR = matrix.length - 1;
int endC = matrix[0].length - 1;
boolean flag = false;
while(tR <= endR){
print(matrix, tR, tC, dR, dC, flag);
tR = tC == endC ? tR + 1 : tR;
tC = tC == endC ? tC : tC + 1;
dC = dR == endR ? dC + 1 : dC;
dR = dR == endR ? dR : dR + 1;
flag = !flag;
}
}
public static void print(int[][] m, int tR, int tC, int dR, int dC, boolean f){
if(f){
while(tR != dR + 1){
System.out.print(m[tR++][tC--] + " ");
}
}else{
while(dR != tR - 1){
System.out.print(m[dR--][dC++] + " ");
}
}
}
//力扣上会出现数组越界问题,有待解决
题目九 在行列都排好序的矩阵中找数
给定一个有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。
给定一个有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。
思路
从右上角或左下角开始比较,以右上角为例,当K小于右上角的值时向下走,当K大于右上角的值时向左走。
代码实现
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;
}
题目十 打印两个有序链表的公共部分
给定两个有序链表的头指针head1和head2,打印两个链表的公共部分。
public static void printCommonPart(Node head1, Node head2){
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();
}
链表的面试笔试要点
面试和笔试不相同,链表问题的最优解一般是用最少的空间搞定。
笔试用最快的方式,不用必须非得空间复杂度低。
面试必须空间复杂度低。
题目十一 判断一个链表是否为回文结构
给定一个链表的头节点head, 请判断该链表是否为回文结构。 例如: 1->2->1, 返回true。 1->2->2->1, 返回true。15->6->15, 返回true。 1->2->3, 返回false。
进阶: 如果链表长度为N, 时间复杂度达到O(N), 额外空间复杂度达到O(1)。
思路及代码实现
笔试时总共有两个思路:
1、额外空间为n:
将所有链表压到栈里,栈中元素一个一个弹出来与链表比较。
public static boolean isPalindromeList1(Node head){
Stack<Node> stack = new Stack<Node>();
Node cur = head;
while(cur != head){
stack.push(cur);
cur = cur.next;
}
while(head != null){
if(head.value == stack.pop().value){
return false;
}
head = head.next;
}
return true;
}
2、额外空间为n/2:
使用快慢指针,快指针走两步,慢指针走一步,当快指针走到头时,慢指针刚好在最中间(奇数个)或者在刚好一半的地方(偶数个)。将慢指针后面的链表压到栈里,栈中元素一个一个弹出来与前一半链表比较。
public static 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<Node>();
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;
}
面试时要求空间复杂度为1,不额外申请空间:
比较麻烦,首先需要设定两个指针,一个快指针,一个慢指针,快指针走到头时,慢指针指向中点,然后将中点的next设为null,将后半部分的链表倒过来,指向中点,设定两个端点的指针,比较value是否相等,走到中点停止,最后一定要把后半部分回归原位
public static 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) { // find mid node
n1 = n1.next; // n1 -> mid
n2 = n2.next.next; // n2 -> end
}
n2 = n1.next; // n2 -> right part first node
n1.next = null; // mid.next -> null
Node n3 = null;
while (n2 != null) { // right part convert
n3 = n2.next; // n3 -> save next node
n2.next = n1; // next of right node convert
n1 = n2; // n1 move
n2 = n3; // n2 move
}
n3 = n1; // n3 -> save last node
n2 = head;// n2 -> left first node
boolean res = true;
while (n1 != null && n2 != null) { // check palindrome
if (n1.value != n2.value) {
res = false;
break;
}
n1 = n1.next; // left to mid
n2 = n2.next; // right to mid
}
n1 = n3.next;
n3.next = null;
while (n1 != null) { // recover list
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
题目十二 将单向链表按某值划分成左边小、 中间相等、 右边大的形式
笔试简单题目:
给定一个单向链表的头节点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的节点即可。 对某部分内部的节点顺序不做要求。
这个题与普通的荷兰国旗问题很像,只不过这道题是链表题,作为笔试题的话,简单解法可以申请额外节点类型数组空间,将链表储存到数组中,进行荷兰国旗问题排序后,再恢复成链表形式即可,可以复习一下初级班1、2中的排序。
public static Node listPartition1(Node head, int pivot) {
if (head == null) {
return head;
}
Node cur = head;
int i = 0;
while (cur != null) {
i++;
cur = cur.next;
}
Node[] nodeArr = new Node[i];
i = 0;
cur = head;
for (i = 0; i != nodeArr.length; i++) {
nodeArr[i] = cur;
cur = cur.next;
}
arrPartition(nodeArr, pivot);
for (i = 1; i != nodeArr.length; i++) {
nodeArr[i - 1].next = nodeArr[i];
}
nodeArr[i - 1].next = null;
return nodeArr[0];
}
public static void arrPartition(Node[] nodeArr, int pivot) {
int small = -1;
int big = nodeArr.length;
int index = 0;
while (index != big) {
if (nodeArr[index].value < pivot) {
swap(nodeArr, ++small, index++);
} else if (nodeArr[index].value == pivot) {
index++;
} else {
swap(nodeArr, --big, index);
}
}
}
public static 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)。
将原来的链表分为三个大部分: small, equal, big,分别设计两个指针指向三部分的头和尾。遍历链表,将三个部分找出后再连在一起,不需要申请额外空间,但是需要注意考虑三个部分为空的情况。
public static Node listPartition2(Node head, int pivot){
Node smallHead = null;
Node smallTail = null;
Node equalHead = null;
Node equalTail = null;
Node bigHead = null;
Node bigTail = null;
Node next = null;
while(head != null){
next = head.next;
if(head.value < pivot){
if(smallHead == null){
smallHead = head;
smallTail = head;
}else{
smallTail.next = head;
smallTail = head;
}
}else if(head.value == pivot){
if(equalHead == null){
equalHead = head;
equalTail = head;
}else{
equalTail.next = head;
equalTail = head;
}
}else{
if(bigHead == null){
bigHead = head;
bigTail = head;
}else{
bigTail.next = head;
bigTail = next;
}
}
head = next;
}
//连接small和equal部分
if(smallTail != null){
smallTail.next = equalHead;
equalTail = equalTail == null ? smallTail : equalTail;
}
//全部连
if(equalTail != null){
equalTail.next = bigHead;
}
return smallHead != null ? smallHead : equalHead != null ? equalHead : bigHead;
}
题目十三 复制含有随机指针节点的链表(难度、妙操作)
一种特殊的链表节点类描述如下:
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, 请实现一个函数完成这个链表中所有结构的复制, 并返回复制的新链表的头节点。
首先需要理解题意,题意的大概意思是,链表除了next指针之外还有一个rand指针,他是随机没有规律的指针,需要输出一个复制的链表的头部。首先笔试中可以使用额外空间HashMap,HashMap是<K,V>结构。将HashMap的key存入原链表节点,value存入新链表节点。这样可通过HashMap找到对应的节点,从key中找指针位置即可。
public static Node copyListWithRand1(Node head){
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;
while (cur != null){
map.get(cur).next = map.get(cur.next);
map.get(cur).rand = map.get(cur.rand);
cur = cur.next;
}
return map.get(head);
}
进阶:
不使用额外的数据结构, 只用有限几个变量, 且在时间复杂度为 O(N)内完成原问题要实现的函数。
通过一个物理结构代替HashMap,将原链表中每个元素的next连接新链表的对应元素(一个节点的拷贝节点放在它的下一个)。下面找两个指针,next指针相对而言较为好找,rand指针需要绕一下。首先先找到一个原节点的rand,找到的这个节点的下一个就是复制节点的rand。最后需要将新老链表进行分离。
public static Node copyListWithRand2(Node head){
Node cur = head;
Node next = null;
while(cur != null){
next = cur.next;
cur.next = new Node(cur.value);
cur.next.next = next;
cur = next;
}
cur = head;
Node curCopy = null;
while(cur != null){
next = cur.next.next;
curCopy = cur.next;
curCopy.rand = cur.rand != null ? cur.rand.next : null;
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;
}
题目十四 两个单链表相交的一系列问题(重点、难点、三道面试题集合)
在本题中, 单链表可能有环, 也可能无环。 给定两个单链表的头节点 head1和head2, 这两个链表可能相交, 也可能不相交。 请实现一个函数, 如果两个链表相交, 请返回相交的第一个节点; 如果不相交, 返回null 即可。 要求: 如果链表1的长度为N, 链表2的长度为M, 时间复杂度请达到 O(N+M), 额外空间复杂度请达到O(1)。
考点1:判断单链表有无环
1、可以用HashSet(没有value的HashMap),遍历链表,判断该结点是否在HashSet里,如果在直接返回该结点或返回true(具体看题目要求),如果不在就写入HashSet。遇到null就不存在环。
2、不用HashSet,准备两个指针,快指针f慢指针s,起初都同时指向头结点,快指针一次走两步,慢指针一次走一步,相遇后,快指针回到头结点,并且每次只走一步,快指针和慢指针会在第一个入环节点处相遇。结论,记住就完事了。
考点2:判断无环单链表相交的第一个结点
1、用HashSet,用上面考点1的方法判断完两个链表都无环之后,将第一个链表写入HashSet里,遍历第二个链表去查HashSet,如果第二个链表都遍历空了,则没有相交。
2、不用HashSet,先遍历链表1,统计链表1的长度len1,拿到链表1最后一个节点end1,然后遍历链表2,链表2的长度len2和最后一个节点end2,先判断end1是否等于end2(内存地址相等,是否是一个节点),如果不相等,则不相交;如果end1=end2,则相交,下面的步骤举例,如果链表1的长度为100,链表2的长度为80,则链表1先走20步,然后链表1和链表2一起走,一定会一起走到第一个相交的节点处。
不存在交叉相交!!!!是单链表结构!!!!!
考点3:两个有环链表相交的第一个结点
两个有环链表相交共有三种情况:
第一种是两链表不相交,第二种第三种情况如下:
得到两个链表的head1和head2,两个链表的第一个入环节点loop1和loop2。
①loop1 == loop2(内存地址一样) ,只能是第二种情况,第二种情况就可以转化为无环链表相交的第一个结点来做
②loop1 != loop2,有可能是第一种情况,也有可能是第三种情况。区分就是loop1往下走,直到遇到loop2,则是第三种情况,若遇不到则没有交点。
代码实现
public class Code_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);
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 s = head.next;
Node f = head.next.next;
while(s != f){
if(f.next == null || f.next.next == null){
return null;
}
s = s.next;
f = f.next.next;
}
f = head;
while(s != f){
s = s.next;
f = f.next;
}
return s;
}
//没有环的情况
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是长的那个链表的头
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);
while(n != 0){
cur1 = cur1.next;
n--;
}
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){
cur1 = cur1.next;
n++;
}
while(cur2 != loop2){
cur2 = cur2.next;
n--;
}
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);
while(n != 0){
cur1 = cur1.next;
n--;
}
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;
}
}