面试题08
问题描述
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
实现思路
先总结出所有可能发生的情况,再根据这些情况来写代码,中序遍历下一节点所有情况如下:
- 节点存在右子树:
- 节点的右孩子存在左子树:
- 则沿着右孩子的左指针一路遍历下去到叶子节点,就是我们要找的值。
- 节点的右孩子不存在左子树:
- 则节点的右孩子即为我们要找的值。
- 节点的右孩子存在左子树:
- 节点无右子树:
- 节点是根节点:
- 直接返回
null
。
- 直接返回
- 节点不是根节点:
- 节点本身是父节点的左孩子:
- 返回父节点。
- 节点是父节点的右孩子:
- 沿着父节点指针一路向上遍历,直到找到一个节点,使得该节点是该节点父节点的左孩子,那么该节点的父节点就是我们要找的值。
(如果找到根节点还没有找到符合条件的节点,则返回null
)
- 沿着父节点指针一路向上遍历,直到找到一个节点,使得该节点是该节点父节点的左孩子,那么该节点的父节点就是我们要找的值。
- 节点本身是父节点的左孩子:
- 节点是根节点:
根据上述情景,来写实现代码:
实现代码
public class Solution {
static class TreeLinkNode {
int val;
TreeLinkNode left = null;
TreeLinkNode right = null;
TreeLinkNode next = null;
TreeLinkNode(int val) {
this.val = val;
}
}
/**
* 找出中序遍历指定节点的下一个节点
* @param pNode 指定节点
* @return 指定节点的下一个节点
*/
public TreeLinkNode GetNext(TreeLinkNode pNode) {
/* 判断是否传入的是空二叉树 */
if(pNode == null) {
return null;
}
/* 判断该节点是否有右子树 */
if(pNode.right == null) {
/* 判断是否为根节点 */
if(pNode.next == null) {
return null;
} else {
/* 判断该节点位于父级节点的左子树还是右子树 */
if(pNode.next.left == pNode) {
return pNode.next;
} else {
/* 根据左根右原则,遍历其父节点,直到找到为父节点左孩子的节点,返回该节点的父节点 */
while(pNode.next.next!=null && pNode.next!=pNode.next.next.left) {
pNode = pNode.next;
}
/* 这里不用判空,因为上述如果没有找到合乎条件的节点,pNode.next.next为null退出循环,自然返回null */
return pNode.next.next;
}
}
} else {
/* 若没有左子树则不进入循环,直接返回原节点的右孩子 */
pNode = pNode.right;
/* 有左子树,则沿着左孩子遍历下去,直到叶子节点就是下一个值 */
while(pNode.left != null) {
pNode = pNode.left;
}
return pNode;
}
}
}
面试题09
问题描述
用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
实现思路
压入操作就压入到栈A中,以栈A为主栈,而栈B为辅助栈。
因为栈是后进先出,而队列是先进先出,正好和栈相反,所以考虑将栈A的值顺序调换。
于是利用栈B,将栈A的值挨个弹出并压入栈B中,则栈B保存的值的顺序正好与栈A相反。
弹出时判断栈B是否为空栈,如果是空栈那么就将栈A的值“倒”入栈B,再弹出栈B位于栈顶的元素。
而如果不是空栈的话,那么直接弹出栈B栈顶的元素即可,因为栈B保存的元素永远是上一次栈A的元素,也就是较早压入的值。
实现代码
public class Solution {
/**
* 声明两个栈并初始化
*/
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
/**
* 用两个栈实现队列的压入操作
* @param node 要压入队列的值
*/
public void push(int node) {
stack1.push(node);
}
/**
* 用两个栈实现队列的弹出操作
* @return 弹出的值
*/
public int pop() {
/* 栈B不为空则直接弹出,为空就把栈A的元素“倒”到栈B,再弹出 */
if(stack2.empty()) {
while(!stack1.empty()) {
stack2.push(stack1.pop());
}
}
/* 在空队列删除元素抛出异常 */
if(stack2.empty()) {
throw new EmptyStackException();
}
return stack2.pop();
}
}
面试题10
题目一
问题描述
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。
n<=39
实现思路
两种实现思路,第一种是用递归的方法,不断将前两项作为入参相加,直到递归调用到最开始的两项。
但是递归调用有个缺陷,在该题中,有大量的重复操作,可以粗略的看一下当n=10
时的递归调用过程:
可以看到,层级调用的越多,重复现象越严重,这对性能的消耗是非常大的。
而最理想的方法就是用循环从下往上调用,用一个变量来不断存储下一项的值。
实现代码
递归实现
public class Solution {
public int Fibonacci(int n) {
/* 基准条件,最开始的两个数为0和1 */
if(n <= 0) {
return 0;
}
if(n == 1) {
return 1;
}
/* 每次递归使下两项相加 */
return Fibonacci(n-1) + Fibonacci(n-2);
}
}
循环实现
public class Solution {
public int Fibonacci(int n) {
if(n < 0) {
throw new IllegalArgumentException("请传入一个非负数。");
}
if(n <= 1) {
return n;
}
int fA = 0;
int fB = 1;
int fTmp = -1;
/* 因为一开始已经有2个数了,初始值为0, 所以以 1 为判断条件 */
while(n != 1) {
n --;
fTmp = fA + fB;
fA = fB;
fB = fTmp;
}
return fTmp;
}
}
另外,看牛客网上的大佬们讨论,没有用中间值一样可以做到,循环的代码是这样的:
while(n != 0) {
n --;
fB += fA;
fA = fB - fA;
}
return fA;
这个思路主要是利用第三项的特性推导出第二项。
因为下一项永远是fA+fB
,所以原来的fB
可以利用下一项减去fA
推导出来。
这样的话因为第一项和第二项要加两次,所以循环次数要加一次。
(因为第一项是0,所以 0,1 要先变成 1,1 再变成 1,2)
题目二
问题描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
实现思路
每一次选择都包含有两种跳法,一阶或是两阶。
如果第一次跳一阶,那么就剩下n-1阶阶梯,传入f(n-1)让这n-1阶阶梯重复判断跳1还是2
如果第一次跳两阶,那么就剩下n-2阶阶梯,传入f(n-2)让这n-2阶阶梯重复判断跳1还是2
每次选择都包含在上述两者之中,所以最终的数目为 f(n-1)+f(n-2),典型的斐波那契数列。
思路与第一题相同,这里不再细说。
实现代码
递归实现
public class Solution {
/**
* 跳台阶递归解法
* @param target 台阶数目
* @return 一共多少种方法
*/
public int JumpFloor(int target) {
if(target <= 0) {
throw new IllegalArgumentException("请传入一个正数。");
}
/* 基准条件,跳1阶有1种,2阶阶梯有2种 */
if(target == 1) {
return 1;
}
if(target == 2) {
return 2;
}
return JumpFloor(target-1) + JumpFloor(target-2);
}
}
循环实现
public class Solution {
/**
* 跳台阶循环解法
* @param target 台阶数目
* @return 一共多少种方法
*/
public int JumpFloor(int target) {
if(target <= 0) {
throw new IllegalArgumentException("请传入一个正数。");
}
if(target <= 2) {
return target;
}
int jA = 1;
int jB = 2;
int jTmp = -1;
/* 因为一开始已经有2个数了,初始从1开始,所以以 2 为判断条件 */
while(target != 2) {
target --;
jTmp = jA + jB;
jA = jB;
jB = jTmp;
}
return jTmp;
}
}
面试题11
问题描述
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
实现思路
考虑到旋转前数组有序,所以利用二分查找法找最小数字。
具体的做法是:
每次用前指针和中指针指向的数比较。
如果前比中大,则最小值可能在数组后半段,否则可能在数组前半段。
这样不断二分查找,最后前后指针只相差一个数时,找到分界点,后指针所指为最小值
(因为前指针指的一直都是较大的值)
注意特殊情况:
数组中没有元素、数组中只有一个元素、原数组中有若干个重复元素如:{1,2,2,2,2,2}。
当旋转后的数组为{2,2,1,2} 时,无法判断较小值在哪里,只能用顺序查找。
顺序查找的思路是:
不断用前指针和后指针所指的数比较,大就让前指针指向下一个值,直到找到分界点。
实现代码
public class Solution {
/**
* 找出旋转数组的最小数字
* @param array 旋转后的数组
* @return 数组中的最小数字
*/
public int minNumberInRotateArray(int [] array) {
if(array.length == 0) {
return 0;
}
/* 设立两个指针,分别指向前面和后面的数组元素 */
int start = 0;
int end = array.length - 1;
int mid = end / 2;
/* 旋转了0次,也就是旋转后的数组是非递减情况 */
if(array[start] < array[end]) {
return array[start];
}
/* 如果三数相等,用顺序查找法,否则用二分查找法 */
if(array[start]==array[mid] && array[mid]==array[end]) {
/* 将数组前几个元素跟最后一个比较,直到找到临界点 */
while(start!=end && array[start]>=array[end]) {
start ++;
}
return array[start];
} else {
/* 用二分法查找数组中最小元素,后一个条件是为了排除数组中只有一个元素的情况 */
while(start!=end-1 && start!=end) {
/* 把数组一分为二,每次比较数组第一个元素和中间元素的值,小就在后者子数组,大就在前者子数组 */
if(array[start] <= array[mid]) {
start = mid;
} else {
end = mid;
}
mid = (end+start) / 2;
}
return array[end];
}
}
}
面试题23
问题描述
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
实现思路
有两种实现思路。
第一种是利用哈希表来存储节点,当节点在哈希表中已经存在时,该节点即为环入口节点。
这种方法因为利用了哈希表作为额外存储空间,所以在空间复杂度上不尽如人意。
第二种则是利用两个指针完成。
首先判断链表中是否有环:
用两个指针,一个每次走两步,一个每次走一步,当他俩相遇的时候,链表有环。
如果走的快的指针走到了null或者其next域为null,链表无环。
其次判断环中节点个数:
让指针B先走,同时设置一个计数器,每走一步计数器加一,当再次走到指针A处时,计数器的值就为环中节点个数。
最后判断环的入口节点:
重新初始化指针A和B,让指针B先走“环中节点个数”个节点,之后他俩同时开始移动。
这样指针B和指针A再次相遇即为环的入口节点。
其实原因就是用指针B去找链表的结尾,先走环的节点个数,那么他和指针A就始终相差一个环的距离,此时当他走到链表的结尾时,因为他们相差一个环的距离,所以指针A应该正好站在环的门口前。
实现代码
哈希表实现
import java.util.HashMap;
public class Solution {
static class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
/**
* 找出链表中环的入口节点
* @param pHead 链表
* @return 环的入口节点(若没有则为null)
*/
public ListNode EntryNodeOfLoop(ListNode pHead) {
if(pHead==null || pHead.next==null) {
return null;
}
/* 初始化一个额外的哈希表来储存节点 */
HashMap<ListNode, Integer> map = new HashMap<>();
while(pHead != null) {
/* 如果包含该节点,则返回,该节点就是环入口 */
if(map.containsKey(pHead)) {
return pHead;
} else {
map.put(pHead, 0);
pHead = pHead.next;
}
}
return null;
}
}
双指针实现
public class Solution {
static class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
/**
* 找出链表中环的入口节点
* @param pHead 链表
* @return 环的入口节点(若没有则为null)
*/
public ListNode EntryNodeOfLoop(ListNode pHead) {
if(pHead==null || pHead.next==null) {
return null;
}
/* 每次走一步的链表指针 */
ListNode pList = pHead;
/* 每次走两步的链表指针 */
ListNode nList = pHead;
/* 确定链表中是否有环 */
while(nList!=null && nList.next!=null) {
nList = nList.next.next;
pList = pList.next;
/* 链表中有环时退出循环 */
if(pList == nList) {
break;
}
}
/* 链表无环的情况 */
if(pList != nList) {
return null;
}
/* 计算环中有几个节点 */
nList = nList.next;
int count = 1;
while(nList != pList) {
nList = nList.next;
count ++;
}
/* 让指针B先走"环中节点个数"个节点 */
pList = pHead;
nList = pHead;
while(count != 0) {
nList = nList.next;
count --;
}
/* 指针AB同时行动,再次相遇的节点即为环的入口节点 */
while(pList != nList) {
pList = pList.next;
nList = nList.next;
}
return pList;
}
}
Github
所有面试题的实现我都会放在我的Github仓库中,包括多种实现与详细注释,需要的可以去以下网址查看:
剑指Offer-Java实现