面试题24
问题描述
输入一个链表,反转链表后,输出新链表的表头。
我的思路
-
在保留头指针的情况下,用三个指针分别指向中间的节点与前后节点,像这样:
-
反转中间指针的指向后,整体后移一格,变成这样:
(这个A节点是头结点,所以不用管A的指向,最后将A结点指为null即可。) -
对上述操作进行循环,直到
n.next
指向null(也就是到了尾结点)为止,此时会变成这样:
最后把c
和n
节点的指向反转,即可。
实现代码
上述思路实现代码如下:
/*public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode ReverseList(ListNode head) {
ListNode headNode = head;
/* 如果头结点为null,返回头结点 */
if(head == null) {
return head;
}
/* 将节点数分别为1、2的情况挑出来,为3个及以上节点分别设置头结点及其前后节点 */
if(head.next == null) {
return head;
} else if (head.next.next == null) {
head = head.next;
head.next = headNode;
headNode.next = null;
return head;
} else {
ListNode nextNode = head.next.next;
head = head.next;
ListNode prevNode = headNode;
/* 每次反转前一个节点的next指向,然后3个指针后移 */
while(nextNode.next != null) {
head.next = prevNode;
prevNode = head;
head = nextNode;
nextNode = nextNode.next;
}
/* 改变最后两个节点next的指向 */
head.next = prevNode;
nextNode.next = head;
head = nextNode;
headNode.next = null;
return head;
}
}
}
其中,headNode
就是保留的原始头结点的指针,即反转链表之后的尾结点指针。
而head
是反转过程中的中间指针,反转之后的头结点指针。
面试题25
问题描述
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
我的思路
- 构建一个最终链表,初始化为
null
。 - 先比较输入的两链表头结点的值的大小,将小的一方的值加入最终链表的新建节点中。
- 遍历两链表,每次循环比较两链表的值,将小的一方的值加入最终链表的新建节点中,直到一个链表遍历完毕。
- 将另一个链表的剩余节点加在最终链表之后,返回最终链表的头结点。
实现代码
方法A
上述思路实现代码如下:
/*class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
/* 判断链表其中一个为null的情况 */
if (list1 == null) {
return list2;
}
if (list2 == null) {
return list1;
}
/* 最终链表指针 */
ListNode finalList = null;
/* 最终链表头指针 */
ListNode headNode = null;
/* 先构造最终链表的头结点 */
if(list1.val >= list2.val) {
finalList = finalList = new ListNode(list2.val);
list2 = list2.next;
} else {
finalList = finalList = new ListNode(list1.val);
list1 = list1.next;
}
headNode = finalList;
/* 遍历两个链表 */
while(list1!=null && list2!=null) {
/* 比较两链表数据大小,将小的一方的值加入最终链表新建节点中 */
if(list1.val >= list2.val) {
finalList.next = new ListNode(list2.val);
list2 = list2.next;
} else {
finalList.next = new ListNode(list1.val);
list1 = list1.next;
}
finalList = finalList.next;
}
/* 一方为空后,直接将另外一个链表的剩余节点加到最终链表上 */
if(list1 == null) {
finalList.next = list2;
} else {
finalList.next = list1;
}
return headNode;
}
}
这个只是初步方法,相信很容易就看出问题,每次都要新建一个节点这也太麻烦了叭,而且最后相当于生成了一条新的链表……
我们能不能不新建节点,直接用现成的节点来完成操作呢?
方法B
一开始我想的是再构造两个链表指针互相比较,而list1
和list2
作为每个链表的头结点,就像这样:
(蓝色是一开始的指向,橘色是改变之后的指向。)
后来发现,行不通…… 简单的示例确实可以通过,但是出现了两个BUG:
- 当两链表有很多相同的值的时候,会漏值。
- 当某链表出现连续的值都比另一链表小时,比如链表A{1, 3, 5},链表B{4, 6},这样就会漏掉3这个值,让1直接指向4。
为了解决这两个BUG,我的想法是:
- 每次相同的值就变换指向,这样就引发了另一个问题:如何判断是改变链表A中节点指向还是改变链表B中节点指向?
- 额外添加一个指针指向后一个节点,把后一个值与另一链表的头结点的值比较,如果小就不更改指向,再比较下一个值。
于是我构建了一个flag
常量用于判断上次变换节点是在链表A还是链表B,如果上次变换节点是由链表A指向了链表B,那么下次相同的值就要从链表B指向链表A。
第一个问题解决了,但是第二个问题我想了一下,我何必要这么麻烦?我直接构建一个过程指针,用flag
判断这个指针当前在哪个链表,然后去跟另一链表的值比较,小就指向下一个节点接着比较,大就将过程指针指向第二个链表然后重新和第一链表比较不就好了?
这样既不用新建节点,当链表A多个连在一起的值比链表B小时,也不用“多此一举”的去把这些节点的指向重新指一遍。
于是,叮叮当当,方法二出炉了:
public ListNode Merge(ListNode list1,ListNode list2) {
/* 判断链表其中一个为null的情况 */
if (list1 == null) {
return list2;
}
if (list2 == null) {
return list1;
}
/* 最终链表的头结点指针 */
ListNode finalList = null;
/* 标志位,标志“过程指针”到了哪个链表,1代表在链表A,2代表在链表B */
int flag = 1;
/* 先把最终链表头结点指向两者中头结点值较小的一方 */
if(list1.val >= list2.val) {
finalList = list2;
flag = 2;
} else {
finalList = list1;
}
/* 两链表过程指针 */
ListNode pList = finalList;
/* 循环直到过程指针指向某一链表最后一个节点 */
while (pList.next != null) {
/* 当过程指针指向A链表时,如果指针下一个节点值比B链表指针小,则过渡到下一个节点,否则切换过程指针所在链表,并将两链表指针后移 */
if(flag == 1) {
if(pList.next.val < list2.val) {
pList = pList.next;
} else {
list1 = pList.next;
pList.next = list2;
pList = list2;
flag = 2;
}
} else {
if(pList.next.val < list1.val) {
pList = pList.next;
} else {
list2 = pList.next;
pList.next = list1;
pList = list1;
flag = 1;
}
}
}
/* 将另一链表的剩余节点加在末尾 */
if(flag == 1) {
pList.next = list2;
} else {
pList.next = list1;
}
return finalList;
}
测试并作双手合十状(阿弥陀佛阿弥陀佛),成功!
(P.S.:我觉得还可以优化,比如有多个相同的值时直接把这两段相同的值连起来,而不用频繁的去变换过程指针,但是那样可能就要套一个循环,或者在体系外增加一个方法,所以此题暂时这样叭)
递归解法
另外,这个题解还有一种思路是用递归来做:
public ListNode Merge(ListNode list1,ListNode list2) {
/* 基准情况,递归遍历直到一方为null */
if(list1 == null){
return list2;
}
if(list2 == null){
return list1;
}
/* 新建最终链表保存结果 */
ListNode finalList = null;
/* 对于每一次遍历的每一个节点来说,最终链表指针永远指向较小的一方 */
if(list1.val < list2.val){
finalList = list1;
finalList.next = Merge(list1.next,list2);
}else{
finalList = list2;
finalList.next = Merge(list1,list2.next);
}
return finalList;
}
这个递归思路很简单,画个图就懂了,这里以前两次递归为例:
蓝色的是每次进入方法新建的指针finalList
(这个引用只存在于当前方法体中,但是它的next域作为下个指针指向节点的返回域),橘色的是第一次进入方法时创建的finalList
,也就是最终链表的情况。
也就是说,每一次调用Merge
方法,都是将上一次连入节点的下一个节点为头结点,作为新链表传入,而返回的结果就加到一开始的finalList
中,持续下去,直到达到基准情况后将另一链表剩余节点返回加到finalList.next
上,最后返回最终链表的头结点。
面试题2
问题描述
设计一个类,我们只能生成该类的一个实例
实现代码
class Singleton {
/**
* 构造方法使用private限定外部实例化该类
*/
private Singleton(){}
/**
* 静态内部私有类
* 在第一次加载时初始化并创建实例
*/
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
/**
* 实例的全局访问点
* @return 唯一实例
*/
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
典型的多线程下的单例模式实现方法,具体可以参考我的另一篇文章:
Java 设计模式通关之路——单例模式
面试题3
问题描述
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
方法一
实现代码
题目的测试用例要求将结果保存在duplication[0]
中,存在返回true
,不存在返回false
。
import java.util.*;
public class Solution {
public boolean duplicate(int numbers[],int length,int [] duplication) {
/* 构建一个哈希表作为中介 */
Map<Integer, Integer> map = new HashMap<>(length);
/* 如果输入为空,直接返回不存在 */
if(length == 0) {
return false;
}
/* 对数组遍历,对每个值判断其在哈希表是否存在,存在就返回,不存在就保存进去 */
for (int num : numbers) {
if(map.containsKey(num)) {
duplication[0] = num;
return true;
} else {
map.put(num, 1);
}
}
return false;
}
}
我的第一想法当然是构建哈希表,对数组中的每一个数字进行判断,不存在就保存在哈希表中,存在就返回。
最简单暴力的想法是对数组排序后遍历,而哈希表的介入简化了这一过程。
继续思考一下,有没有一种方法可以不需要任何的介入呢?
方法二
重新审题,有一句话引起了我的注意:所有数字都在0到n-1的范围内。
题目为什么要给出这句话?显然是有原因的,这句话的意思变换一下就是:如果数组中没有重复的数字,那么当数组排序后,数组的下标就是对应的值!
一个思路渐渐清晰起来:我们可以让每一个数字都放在他应该在的位置,这样只要其他位置只要有一样的值,就是重复的。
说起来有点绕,举个例子,现在有一个数组:list {2, 1, 3, 0, 3}
。
- 先看第一个值
2
,它应该在哪?它应该在数组下标为2
的位置,所以我们先判断list[2]
的位置是不是也是2
,发现不是,那么就将2
换到它该在的位置上。
换过一次后数组变为:list {3, 1, 2, 0, 3}
。 - 继续检测
3
这个数,对应位置的数为0
,不相等,交换:list {0, 1, 2, 3, 3}
。 - 检测
0
,发现他就在本来应该在的位置,扫描下一个,一直扫描到最后一个数,也就是list[4]
位置的数3
,拿它和list[3]
比较,相等!重复了,返回这个数。
接下来看实现代码。
实现代码
public class Solution {
public boolean duplicate(int numbers[],int length,int [] duplication) {
for(int i=0; i<length; i++) {
/* 如果值在对应位置上,跳过本次循环看下一个值 */
if(numbers[i] == i) {
continue;
} else {
/* 如果值与对应位置上的值相等,则重复,否则交换位置,重新检测(i-1) */
if(numbers[i]==numbers[numbers[i]]) {
duplication[0] = numbers[i];
return true;
} else {
int temp = numbers[i];
numbers[i] = numbers[numbers[i]];
numbers[temp] = temp;
i --;
}
}
}
return false;
}
}
面试题4
问题描述
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
我的思路
因为基本没有做过二维数组的题,所以初次见到的时候还是有点害怕的。
理了一下题目,大致在脑海中构思了一个想法(但是估计是最笨的雾):
- 先拿整数和二维数组的最大值比较,也就是右下角那个值,大于或者等于当然是最好,大于代表不存在,等于就直接返回了。
- 小于的话,考虑到二维数组可以分割成若干个一维数组,所以按照最后一列向上遍历,直到确定所在的最小一维数组。
比如最后一列是{9, 12, 15, 20}
,20
作为二维数组最大值已经比较过,所以和15
去比较,如果大于15
就从最后一行开始找,也就是20
所在的一维数组,小于15
就继续往下跟12
比。 - 经过第二步操作确定了那个数在哪几行之中。
(比如大于12
小于15
,那么这个数必然在15
所在的一维数组或者比15
大的下面那些行中)
之后再按照最小的这个一维数组往前遍历(遍历15
所在行),直到找到比这个整数小的(它都比这个整数小,那前面的就更小了),再去遍历这个数所在的列,找到比这个整数大的,再遍历行,重复以上操作。
写思路都快把我写晕了…
其实很简单,就是跟蛇形步一样,拐来拐去,从最小的行中最大的数开始按行拐,遇到较小的数就往下按列拐(因为从上到下单调递增),遇到较大的数再往左按行拐(因为从右到左单调递减),直到找到这个数或者遍历完,画个图可能清晰一点……
假如我在这个二维数组中找整数3
,则遍历路径为:
从15
所在列比较,小小小,到最后一行,还是小;
那就在9
所在行比较,小,大!比2
大!
所以接着沿着2
所在列比较,下一个数,比4
要小!所以又在4
所在行比较。
发现,比2
大,所以沿着2
所在列比较,找到了3
相等,返回。
永远参考一个原则:对于二维数组中的某一个数来说,下面的数比它大,左边的数比它小。
我从右下角最大的数开始往上遍历每一行最大的值,若存在与该整数相等的值,则一定能去遍历到。
实现代码
public class Solution {
public boolean Find(int target, int [][] array) {
/* 得到行和列的最大下标 */
int columnIndex = array.length - 1;
int rowIndex = array[0].length - 1;
/* 如果传入的二维数组为空,返回false */
if(columnIndex == 0 && rowIndex == -1) {
return false;
}
/* 如果该整数比二维数组中最大值还大,返回false */
if(target > array[columnIndex][rowIndex]) {
return false;
}
/* 遍历二维数组最后一列,直到找到大于等于该整数的值或者遍历到第一行 */
while(target < array[columnIndex][rowIndex] && columnIndex!=0) {
columnIndex --;
}
/* 如果等于就返回true,否则遍历该数所在的一维数组(所在行) */
if(target == array[columnIndex][rowIndex]) {
return true;
} else {
/* 如果不是第一行(比如小于第四行的15但是大于第三行的13),则遍历较大的数(15)所在行 */
if(columnIndex != 0) {
columnIndex ++;
}
/* 从后往前遍历上述找出的数所在的一维数组(行) */
while(target != array[columnIndex][rowIndex]) {
rowIndex --;
/* 该行遍历完了或者该列遍历完了还没跳出循环,证明没有相等的数,返回false */
if(rowIndex == -1 || columnIndex > (array.length-1) ) {
return false;
}
/* 当在该行中找到小于target整数的数时,遍历该列,直到找到大于target的数 */
while(target > array[columnIndex][rowIndex] && columnIndex <= (array.length-1)) {
columnIndex ++;
}
}
return true;
}
}
}
优化代码
看了一下书,发现思路相同…… 书上是从右上角开始遍历,而我是从最大值也就是右下角开始遍历。
再看一下代码示例…… 为什么要比我的简单这么多?!
我用了一个嵌套的循环,而它用了一个循环就解决了,先贴代码示例,再来分析一下我的代码“复杂”在哪里:
public class Solution {
public boolean Find(int target, int [][] array) {
if (array == null) {
return false;
}
int rowIndex = 0;
int columnIndex = array[0].length - 1;
while (rowIndex<array.length && columnIndex>=0) {
if (target == array[rowIndex][columnIndex]) {
return true;
} else if (target < array[rowIndex][columnIndex]) {
columnIndex --;
} else {
rowIndex ++;
}
}
return false;
}
}
因为从右上角开始遍历,所以是从array[0][columnIndex]
开始。
他的遍历条件是:只有当遍历完最后一行最后一列时才会退出循环。
而我的遍历条件,是以是否找到这个值为基准。
这个谁先谁后我不知道有没有一个标准的定论,我们都在循环中包含了另一个条件判断,所以我觉得没有什么大的差别,不过外层循环尽量用“大范围”的条件可能要好一点。
我加了一个循环的原因是,他是每次循环遍历一个数,而我在里面直接用循环把该列遍历完了。
所以显而易见,我应该改进的地方:
- 尽量在外层循环使用“大范围”的条件。
比如我的外层循环的条件是:找到相等的数,而内部条件判断是:是否遍历完全。“找到相等的数”相比于“是否遍历完全”可能范围要更小,所以应该把“是否遍历完全”放在外层循环上,虽然无甚差别,但是从逻辑上来讲由大到小会更通一点。 - 能用外层循环做的事情,就不要在内层多此一举。
因为题目中给出的性质已经决定了“下大左小”,所以比它大往下,比它小往左,完全可以用条件判断来解决,没必要再多加一层循环。 - 根据性质确定起始点。
由于“下大左小”的性质,起始点的最佳选择当然是右上角的数(因为它的右边和上边没有数,只用判断下和左即可,小往左大往下),而我顺着第一感觉选了右下角的二维数组最大值,这是不正确的,当提取了“性质”后,根据性质来选择起始点才是最佳的。
Github
所有面试题的实现我都会放在我的Github仓库中,包括多种实现与详细注释,需要的可以去以下网址查看:
剑指Offer-Java实现