文章目录
一、数组实现队列与栈
题目描述:用数组结构实现大小固定的栈和队列。
相当于一面第一题的难度,验证面试者不是傻子。
确保线程互斥的访问同步代码
1. 数组实现栈
解题思路:初始化一个大小固定的数组,并定义变量 idnex 指向栈顶元素的下一位置。进栈操作,将进栈元素存入 index 指向的位置,然后 index++;出栈操作, --index,然后返回 index 指向位置的元素,返回后是否删除该位置的元素都可以,因为下一次进栈的元素会覆盖掉现有元素。
代码实现:
public static class ArrayStack {
private Integer[] arr;
private Integer index; // index指向栈顶元素的下一位置
public ArrayStack(int initSize) {
if (initSize < 0) {
throw new IllegalArgumentException("The init index is less than 0");
}
arr = new Integer[initSize];
index = 0;
}
public Integer peek() {
if (index == 0) {
return null;
}
return arr[index - 1];
}
public void push(int obj) {
if (index == arr.length) {
throw new ArrayIndexOutOfBoundsException("The queue is full");
}
arr[index++] = obj;
}
public Integer pop() {
if (index == 0) {
throw new ArrayIndexOutOfBoundsException("The queue is empty");
}
return arr[--index];
}
}
2. 数组实现队列
解题思路:初始化一个大小固定的数组 arr,定义变量 start = 0,end = 0。end 表示如果新加入一个数,应该放入哪个位置;start 表示如果取一个数,应该返回哪个位置的数。然后再设计一个变量 size = 0, 约束 start 和 end 的行为。如果 size < arr.length,就表示可以加入一个数放在 end 的位置上,然后 end++; 如果 size != 0 ,就表示可以将 start 的位置上的数返回,然后 start++。注意,end 的运动逻辑是,一旦到底就回到开头。
size 变量的作用是使 end 与 start 相互解耦,逻辑更加清晰,代码更加易写。可以省掉 size,但是抠边界条件很烦。在面试场合,使代码正确易写才是最重要的。
代码实现:
public static class ArrayQueue {
private Integer[] arr;
private Integer size; // 数组中已经存入多少个数
private Integer start;
private Integer end;
public ArrayQueue(int initSize) {
if (initSize < 0) {
throw new IllegalArgumentException("The init index is less than 0");
}
arr = new Integer[initSize];
size = 0;
start = 0;
end = 0;
}
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++;
arr[end] = obj;
end = (end == arr.length - 1) ? 0 : end + 1;
}
public Integer poll() {
if (size == 0) {
throw new ArrayIndexOutOfBoundsException("The queue is empty");
}
size--;
int tmp = start;
start = (start == arr.length - 1) ? 0 : start + 1;
return arr[tmp];
}
}
二、实现包含getMin函数的栈
题目描述:实现一个特殊的栈, 在实现栈的基本功能的基础上, 再实现返回栈中最小元素的操作。
要求:
(1)pop、push、getMin操作的时间复杂度都是O(1)
(2)设计的栈类型可以使用现成的栈结构
相当于一面第一题的难度,验证面试者不是傻子。
《剑指Offer》面试题21:包含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.stackData.isEmpty()) {
throw new RuntimeException("Your stack is empty.");
}
this.stackMin.pop();
return this.stackData.pop();
}
public int getmin() {
if (this.stackMin.isEmpty()) {
throw new RuntimeException("Your stack is empty.");
}
return this.stackMin.peek();
}
}
三、栈与队列互相实现
一面难度。《剑指Offer》2.3.5:栈和队列
面试题7:用两个栈实现队列
扩展题:用两个队列实现栈
1. 队列结构实现栈结构
题目描述:如何仅用队列结构实现栈结构?
解题思路:使用两个队列结构 Data(Queue1) 和 Help(Queue2),push 操作一样,添加push() 到Data,pop() 核心是把 Data 除最后添加的元素"剪切"到 Help ,最后poll()剩下的第一个元素,即为最后添加的元素,最后 再交换Data和Help和引用。
代码实现:
public static class TwoQueuesStack {
private Queue<Integer> data;
private Queue<Integer> help;
public TwoQueuesStack() {
data = new LinkedList<Integer>();
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());
}
int res = data.poll();
help.add(res);
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 res;
}
private void swap() {
Queue<Integer> tmp = help;
help = data;
data = tmp;
}
}
2. 栈结构实现队列结构
题目描述:如何仅用栈结构实现队列结构?
解题思路:
倒数据的行为可以发生在任何时刻,只要遵守两个原则,
(1)如果push栈决定要将数倒往pop栈,一次必须倒完;
(2)如果pop栈有数,push一定不能倒。
代码实现:
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);
daoData();
}
public int poll() {
if (stackPop.empty() && stackPush.empty()) {
throw new RuntimeException("Queue is empty!");
} else if (stackPop.empty()) {
while (!stackPush.empty()) {
stackPop.push(stackPush.pop());
}
}
daoData();
return stackPop.pop();
}
public int peek() {
if (stackPop.empty() && stackPush.empty()) {
throw new RuntimeException("Queue is empty!");
} else if (stackPop.empty()) {
while (!stackPush.empty()) {
stackPop.push(stackPush.pop());
}
}
daoData();
return stackPop.peek();
}
public void daoData() {
if(!stackPop.isEmpty()) {
return;
}
while(!stackPush.isEmpty()) {
stackPop.push(stackPush.pop());
}
}
}
四、猫狗队列问题
一面难度。
题目描述:宠物、 狗和猫的类如下,
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");
}
}
实现一种狗猫队列的结构, 要求如下:
(1)用户可以调用add方法将cat类或dog类的实例放入队列中;
(2)用户可以调用pollAll方法,将队列中所有的实例按照进队列的先后顺序依次弹出;
(3)用户可以调用pollDog方法,将队列中dog类的实例按照进队列的先后顺序依次弹出;
(4)用户可以调用pollCat方法,将队列中cat类的实例按照进队列的先后顺序依次弹出;
(5)用户可以调用isEmpty方法,检查队列中是否还有dog或cat的实例;
(6) 用户可以调用isDogEmpty方法,检查队列中是否有dog类的实例;
(7) 用户可以调用isCatEmpty方法,检查队列中是否有cat类的实例.
解题思路:思路简单,设计计数器,封装数据结构即可。详见视频。
代码实现:
// 将进入的pet对象与对应的时间戳封装为PetEnter数据结构
public static class PetEnter {
private Pet pet;
private long count; // 计数器,表示第几个进入
public PetEnter(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<PetEnter> dogQ;
private Queue<PetEnter> catQ;
private long count;
public DogCatQueue() {
this.dogQ = new LinkedList<PetEnter>();
this.catQ = new LinkedList<PetEnter>();
this.count = 0;
}
public void add(Pet pet) {
if (pet.getPetType().equals("dog")) {
this.dogQ.add(new PetEnter(pet, this.count++));
} else if (pet.getPetType().equals("cat")) {
this.catQ.add(new PetEnter(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();
}
}
五、顺时针打印矩阵
一面难度。《剑指Offer》面试题20:顺时针打印矩阵
题目描述:输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数组。
解题思路:不要将思维限制在局部细节,锻炼宏观的调度能力。
代码实现:
public class Code_06_PrintMatrixSpiralOrder {
public static void spiralOrderPrint(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 curR = tR;
int curC = tC;
// 从左往右,打印除了最后一个数的所有数
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--;
}
}
}
public static void main(String[] args) {
int[][] matrix = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 },
{ 13, 14, 15, 16 } };
spiralOrderPrint(matrix);
}
}
六、旋转正方形矩阵90度
一面难度。
题目描述:给定一个整型正方形矩阵matrix, 请把该矩阵调整成顺时针旋转90度的样子。
【要求】 额外空间复杂度为O(1),不申请额外空间,原地处理。
解题思路:宏观思路代替微观,设计顺时针旋转外圈的方法,从外到里依次旋转每一圈。
代码实现:
public class Code_05_RotateMatrix {
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 tmp = 0;
for (int i = 0; i != times; i++) {
tmp = 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] = tmp;
}
}
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] + " ");
}
System.out.println();
}
}
}
七、反转单向和双向链表
一面难度。《剑指Offer》面试题19:反转链表
题目描述:分别实现反转单向链表和反转双向链表的函数。
【要求】如果链表长度为N,时间复杂度要求为O(N), 额外空间复杂度要求为O(1)
解题思路:详见《剑指Offer》。
代码实现:
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode ReverseList(ListNode head) {
if (head == null) {
return null;
}
ListNode pAhead = null; // 指向前一个结点
while(head != null) {
ListNode pBehind = head.next; // 保存当前结点的后一个结点
head.next = pAhead; // 调整当前结点的指针
pAhead = head;
head = pBehind;
}
return pAhead;
}
}
八、“之” 字形打印矩阵
一面难度。
题目描述:给定一个矩阵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。
解题思路:总体的方法论是,宏观思路代替微观,把宏观思路设计好,不要把思维限制在一个位置如何变化,很难描述。
其实是介绍了一种思路:如果局部位置不好写,一定有宏观过程。思路发散的过程中,对于每一步的思维要有一个评估,当发现局部位置的确有规律,但是短时间内很难总结出来,是否可以换成宏观思维来考查。
代码实现:
public static void printMatrixZigZag(int[][] matrix) {
int aR = 0; // a点的行号
int aC = 0; // a点的列号
int bR = 0; // b点的行号
int bC = 0; // b点的列号
int endR = matrix.length - 1; // 行的终止位置
int endC = matrix[0].length - 1; // 列的终止位置
boolean fromUp = false; // 标志位,确定从左上往右下打印,还是从右下往左上打印
while (aR != endR + 1) {
printLevel(matrix, aR, aC, bR, bC, fromUp);
// 打印一次后,a点、b点、标志位变化逻辑
aR = aC == endC ? aR + 1 : aR;
aC = aC == endC ? aC : aC + 1;
bC = bR == endR ? bC + 1 : bC;
bR = bR == endR ? bR : bR + 1;
fromUp = !fromUp;
}
System.out.println();
}
public static void printLevel(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++] + " ");
}
}
}
九、在行列都排好序的矩阵中找数
一面难度。《剑指Offer》面试题2:二维数组中的查找
题目描述:给定一个有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)。
解题思路:从左上角点或右下角点开始找。详见《剑指Offer》。
代码实现:
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;
}
十、打印两个有序链表的公共部分
一面难度。《剑指Offer》
题目描述:给定两个有序链表的头指针head1和head2,打印两个链表的公共部分。
解题思路:类似于“合并两个有序链表”。
代码实现:
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 { // head1.value == head2.value
System.out.print(head1.value + " ");
head1 = head1.next;
head2 = head2.next;
}
}
System.out.println();
}
十一、判断一个链表是否为回文结构
一面难度。《剑指Offer》
题目描述:给定一个链表的头节点head,请判断该链表是否为回文结构。例如:1->2->1,返回true。1->2->2->1,返回true。
15->6->15,返回true。1->2->3,返回false。
要求: 如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1)。
解题思路:回文结构的含义是,存在一个对称轴,对称轴两侧是逆序关系(回文中心的两侧互为镜像)。
public static class Node {
public int value;
public Node next;
public Node(int data) {
this.value = data;
}
}
第一种方法(笔试中,可以使用辅助空间):
如果允许使用额外空间,在第一次遍历过程中,将每个结点放入辅助栈中,那么栈弹出的顺序就是逆序。第二次遍历时,同步地从栈中弹出元素。如果每一步比对值都相等,则返回true;否则返回false。额外空间复杂度为O(N)。
代码实现:
// need n extra space
public static boolean isPalindrome1(Node head) {
if (head == null || head.next == null) {
return true;
}
Stack<Node> stack = new Stack<Node>();
Node cur = head;
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
while (head != null) {
if (head.value != stack.pop().value) {
return false;
}
head = head.next;
}
return true;
}
第二种方法(改进第一种方法):
双指针法。快指针一次走两步,慢指针一次走一步。快指针走完时,慢指针来到中点位置。遍历慢指针右边的部分并压栈。再从头开始遍历,同步地从栈中弹出元素。如果栈弹完了都没有出现不匹配的情况,即每一步比对值都相等,则返回true;否则返回false。相当于只将后半段逆序,再与前半段比较。这种方法省掉了一半的辅助栈空间,但忽略掉常数项,额外空间复杂度还是O(N)。
代码实现:
// need n/2 extra space
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;
}
第三种方法(面试中,不辅助空间):
双指针法。快指针一次走两步,慢指针一次走一步。快指针走完时,慢指针来到中点位置。接下来,将右边部分逆序,如下图。然后,重置两个指针,分别指向链表首尾,一次移动一步并比对是否一样,移动到中点位置停止。如果中间比对存在不一样的情况,返回false;如果移动到中点位置,全都一样,返回true。当然,不管最后返回false还是true,都要将右边部分调整回来。
代码实现:这一段代码的边界没有抠出来
// need O(1) extra space
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;
}
Q:偶数、奇数个数的链表怎么抠中点?
A:保证奇数个的情况下,慢指针正好来到中间元素的位置。保证偶数个的情况下,中间有两个元素,慢指针正好来到前一个元素的位置。
十二、将单向链表按某值划分成左边小、中间相等、右边大的形式
一面难度。《剑指Offer》
题目描述:给定一个单向链表的头节点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的节点即可。对某部分内部的节点顺序不做要求。
解题思路:荷兰国旗问题,只不过是链表形式。一种最快的方式是,把所有链表结点放入数组中,然后使用荷兰国旗问题的代码将数组调成左边小、中间相等、右边大的顺序,再重新连接起来。
代码实现:
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;
}
【进阶】:在原问题的要求之上,再增加如下两个要求,
(1)要求做到稳定性:在左、中、右三个部分的内部也做顺序要求,要求每部分里的节点从左到右的顺序与原链表中节点的先后次序一致。
例如:链表9->0->4->5->1,pivot=3。调整后的链表是0->1->9->4->5。在满足原问题要求的同时,左部分节点从左到右为0、1。在原链表中也是先出现0,后出现1;中间部分在本例中为空,不再讨论;右部分节点 从左到右为9、4、5。在原链表中也是先出现9,然后出现4,最后出现5。
(2)如果链表长度为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)。
解题思路:首先,荷兰国旗问题做不到稳定性。其次,必须使用数组,额外空间复杂度O(N)。要做到这两点,需要传递给面试官以下信息,
(1)知道什么是稳定性;
(2)知道荷兰国旗问题做不到稳定性;
(3)知道链表问题可以省掉空间做;
(4)coding能力达标。
解决该问题的思想,将大链表拆为3个保持原始相对次序的小链表,然后首尾相连,具体步骤如下,
(1)传入参数是链表头结点head和一个数num,定义三个结点类型的变量less、equal、more,都初始化为空。
(2)第一次遍历链表。找到第一个小于num的结点,使less指向它; 找到第一个等于num的结点,使equal指向它;找到第一个大于num的结点,使more指向它。如果有不存在的情况,则对应变量仍然为空。这一步相当于准备了三个区域。
(3)第二次遍历链表。在每个区域定义变量end,分别初始化为less、equal、more。如果遇到小于num的结点,先判断是否为less,如果是,不做处理;如果不是,将该结点挂在end的next指针上,并修改end指向当前结点。遇到等于、大于num的情况,处理过程相同。实质就是,在每个区域准备了两个变量,链表重连很容易。 因为在遍历过程中,每次遇到三种情况之一的结点,都是将该结点连在对应区域的尾部,所以在各个区域中一定保持原相对次序不变,做到了稳定性。
(4)上述过程完成之后,连接小于区域的尾部与等于区域的头部,连接等于区域的尾部与大于区域的头部,整体就连好了。
注意,可能存在某个区域没有结点,应该怎么处理边界,这是coding的细节。
代码实现:
public static Node listPartition2(Node head, int pivot) {
Node sH = null; // small head
Node sT = null; // small tail
Node eH = null; // equal head
Node eT = null; // equal tail
Node bH = null; // big head
Node bT = null; // big tail
Node next = null; // save next node
// every node distributed to three lists
while (head != null) {
next = head.next;
head.next = null;
if (head.value < pivot) {
if (sH == null) {
sH = head;
sT = head;
} else {
sT.next = head;
sT = head;
}
} else if (head.value == pivot) {
if (eH == null) {
eH = head;
eT = head;
} else {
eT.next = head;
eT = head;
}
} else {
if (bH == null) {
bH = head;
bT = head;
} else {
bT.next = head;
bT = head;
}
}
head = next;
}
// small and equal reconnect
if (sT != null) {
sT.next = eH;
eT = eT == null ? sT : eT;
}
// all reconnect
if (eT != null) {
eT.next = bH;
}
return sH != null ? sH : eH != null ? eH : bH;
}
十三、复制含有随机结点指针的链表
一面难度。《剑指Offer》面试题26:复杂链表的复制
题目描述:一种特殊的链表节点类描述如下,
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, 请实现一个函数完成这个链表中所有结构的复制,并返回复制的新链表的头节点。
解题思路:使用哈希表HashMap,遍历两次链表,步骤如下,
(1)第一次遍历链表,依次复制结点。并将原生结点与复制结点作为HashMap的<K,V>对象存储。
(2)第二次遍历链表,根据哈希表存储的信息设置复制结点的 next指针 和 rand指针。
代码实现:
public static Node copyListWithRand1(Node head) {
if (head == null) {
return null;
}
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(1),且在时间复杂度为O(N)内完成原问题要实现的函数。
解题思路:
public static Node copyListWithRand2(Node head) {
if (head == null) {
return null;
}
// 复制结点,并按如下规则连接:原生Node1 -> 复制Node1 -> 原生Node2 -> 复制Node2 ...
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; // next指向cur的下一个原生结点
curCopy = cur.next; // curCopy指向cur的复制结点
curCopy.rand = cur.rand != null ? cur.rand.next : null; // 设置curCopy的rand指针
cur = next; // 调整cur,指向下一个原生结点
}
// 拆分链表
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;
}
十四、两个单链表相交的一系列问题
一面难度,Medium。
题目描述:在本题中,单链表可能有环,也可能无环。给定两个单链表的头节点 head1 和 head2,这两个链表可能相交,也可能
不相交。请实现一个函数,如果两个链表相交,请返回相交的第一个节点;如果不相交,返回null 即可。
【要求】 如果链表1的长度为N,链表2的长度为M,要求时间复杂度为 O(N+M),额外空间复杂度请达到O(1)。
解题思路:这个问题较为复杂,是三道面试题的综合,无良面试官根本不引入单链表有环还是无环的概念。。
第一个问题:怎么判断一个单链表有环还是无环?
第二个问题:怎么得到两个无环单链表第一个相交的结点?
第三个问题:怎么得到两个有环单链表第一个相交的结点?
Q1:怎么判断一个单链表有环还是无环?
解决第一个问题,我们的设计思路是:如果单链表有环,返回第一个入环结点;如果单链表无环,返回null。
第一种方法,使用哈希表。从head结点开始,通过next指针遍历链表的过程中,将每一个结点放入HashSet中,同时检查该结点是否已经存在于HashSet中,如果存在,则单链表有环,且该结点为第一个入环结点,如下图所示。如果遍历到null,则单链表无环。
第一种方法,不使用哈希表,快慢指针法。准备两个指针,一个快指针Fast,一个慢指针Slow。快指针一次走两步,慢指针一次走一步。
(1)如果快指针在遍历过程中遇到null,直接返回无环;
(2)如果有环,快慢指针一定会在环上的某一个结点相遇。相遇之后,快指针重置为头指针,然后快指针的步长变为一次走一步。快慢指针一定会在第一个入环结点处相遇(一个结论,可以证明,但数学证明比较麻烦)。
Q2:怎么得到两个无环单链表第一个相交的结点?
两个单链表的头节点 head1 和 head2,分别调用上述方法,得到代表链表有环还是无环的loop1和loop2。
第一种方法,使用哈希表。如果loop1null && loop2null,说明两个链表无环。把链表1所有的结点放入HashSet中。然后遍历链表2,检查链表2的每一个结点是否在HashSet中,第一个存在的就是第一个相交的结点;如果链表2遍历到null,仍然没有找到相交结点,说明不相交。
第二种方法,不使用哈希表。遍历链表1,统计链表1的长度 len1 以及得到链表1的最后一个结点 end2。同理遍历链表2,得到l链表2的长度 len2 和链表2的最后一个结点 end2。先判断end1 == end2?如果 end1 != end2,不可能相交;如果 end1==end2,说明相交,但是最后一个结点未必是第一个相交的结点。怎么找到第一个相交结点呢?sub = len1 - len2,长的链表先走sub步,然后一起走,一定会共同走到第一个相交结点处。
如果一个链表有环,一个链表无环,不可能相交。
Q3:怎么得到两个有环单链表第一个相交的结点?
两个链表都有环,有三种拓扑关系。
代码实现: