面试题22
问题描述
输入一个链表,输出该链表中倒数第k个结点。
我的思路
设置一个指针来遍历链表,同时设置一个指针指向遍历指针的第前k个节点。
比如n指针(遍历指针)和p指针初始化指向链表头部。
n指针从链表头部向后遍历,同时设置一个计数器记录移动次数。
当计数器的值等于k时,让p指针同步开始移动(此时p指针指向的节点即为n指针的第前k个节点)
当n指针遍历完全链表时,返回p指针指向的节点。
同步设置一个整数型变量记录链表长度,如果链表长度小于k时,返回null
。
实现代码
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
/* 遍历指针 */
ListNode nList = head;
/* 遍历指针前k个节点的指针 */
ListNode pList = head;
/* 保证k为合理参数 */
if(k <= 0) {
throw new IllegalArgumentException("请输入大于0的整数k。");
}
/* 链表长度 */
int length = 0;
/* 遍历指针移动次数 */
int count = 0;
while(nList != null) {
nList = nList.next;
length ++;
/* 在遍历指针移动了k次时,同步移动遍历指针前k个节点的指针 */
if(count != k) {
count ++;
} else {
pList = pList.next;
}
}
if(length < k) {
return null;
} else {
return pList;
}
}
}
注意点
考虑以下三种情况的发生:
- 输入的
head
为null
的情况。
如果head
为null
,则pList
和nList
均为null
,因为判断条件以nList
是否为null
为基准,所以不会进入循环,无论输入的k是多少,均返回null
。 - 节点总数少于
k
。
最后用length
与k
进行比较,若k
大于length
,返回null
。 - 输入的参数
k
为不合理参数。
假如输入的k
为非正数,则抛出非法参数异常(测试用例可能需要返回null
)。
面试题18
题目一
问题描述
在O(1)时间内删除链表节点。
实现思路
要求在O(1)时间内完成,那肯定不能加循环了,怎么办?
注意到预删除节点是直接给出来的,而删除节点除了改变前驱节点next域的指向外,还有一种方法就是将下一个节点的值赋予到该节点,再以改变指针的方法删除下一个节点,此时直接对预删除节点和之后节点进行操作,不用进入循环。
实现代码
public class Solution {
/**
* 链表节点内部类
*/
static class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
/**
* 删除节点
* @param pHead 链表头结点指针
* @param pToBeDeleted 链表要删除的节点
*/
public void deleteNode(ListNode pHead, ListNode pToBeDeleted) {
/* 传入空节点的话直接停止调用 */
if(pHead==null || pToBeDeleted==null) {
return;
}
/* 在默认链表中存在要删除节点的情况下,链表中只有一个节点 */
if(pHead.next == null) {
/* 这里未做任何操作的原因是: Java是值传递,方法中传参只是拷贝的引用,不会影响到实际引用,而题目又要求返回void,故实际无法将唯一节点置null */
return;
}
/* 该节点是尾结点 */
if(pToBeDeleted.next == null) {
while(pHead.next != pToBeDeleted) {
pHead = pHead.next;
}
pHead.next = null;
return;
}
/* 正常情况,将后者的值赋予前者,然后删除后者 */
pToBeDeleted.val = pToBeDeleted.next.val;
pToBeDeleted.next = pToBeDeleted.next.next;
}
/**
* 打印链表
* @param pHead 链表头节点的指针
*/
private static void printList(ListNode pHead) {
if(pHead == null) {
System.out.println("null");
}
while(pHead.next != null) {
System.out.print(pHead.val + ",");
pHead = pHead.next;
}
System.out.println(pHead.val);
}
/**
* 手动创建测试用例,考虑三种情况:
* 1. 正常情况
* 2. 删除头节点
* 3. 删除尾节点
*/
public static void main(String[] args) {
ListNode pHead = new ListNode(0);
ListNode firstNode = new ListNode(1);
ListNode secondNode = new ListNode(2);
ListNode thirdNode = new ListNode(3);
pHead.next = firstNode;
firstNode.next = secondNode;
secondNode.next = thirdNode;
thirdNode.next = null;
Solution solution = new Solution();
System.out.print("开始链表:");
printList(pHead);
System.out.print("删除其中一个节点后:");
solution.deleteNode(pHead, firstNode);
printList(pHead);
System.out.print("删除头节点后:");
solution.deleteNode(pHead, pHead);
printList(pHead);
System.out.print("删除尾节点后:");
solution.deleteNode(pHead, thirdNode);
printList(pHead);
}
}
由于网上没有现成的测试用例,所以手动构造极端情况的测试用例加以测试。
注意点
- 链表中无节点。
判断后返回null
。 - 链表中只有一个节点。
由于题目要求返回为void
,故无法将对应引用置空,加注释标志。 - 删除的节点为尾结点。
此时无法用该方法删除,因为next
域为null
,只能用第一种改变前驱指针的做法。
题目二
问题描述
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
实现思路
因为重复节点不保留,所以设置遍历节点的前驱节点,当遍历到重复节点时,删除重复节点。
有几个要特别注意的地方,引发了几种特殊的情况:
- 链表中全是重复节点,如:{1, 1, 1, 1, 1}
- 链表中开头是重复的节点,如:{1,1,2,2,3,4}
- 链表中开头是重复的节点且后续只有一个节点,如:{1,1,1,1,2}
- 链表中从开头到结尾全是不同的重复节点,如:{1,1,2,2,3,3,4,4}
针对这几种情况,一开始我是在程序中加入“是否头节点重复判断”、“是否有重复节点标志位”等来区别,后来通过是通过了,但是嵌套循环中包含了5个条件判断,代码过于复杂,参考牛客网上大佬的评论,优化思路如下:
构建一个新的头指针节点,此时“头节点值重复”的问题就转换为了一般问题。
因为最后返回的是新的头节点的next
域,所以不用考虑新的头节点的值是否与头节点的值相同的问题。
最后一句话有点绕,我举个例子吧,比如链表为:1,1,2,3,4
我新增的头节点的值也为1,那么新链表就是:1,1,1,2,3,4
但是因为我是从头节点以后去找重复值,不管新头节点的值,所以删除之后链表为:1,2,3,4
返回新头节点的next
域,最终返回的链表为:2,3,4
也就是新的头节点的值与最终结果无关,其加入只是为了将问题一般化。
实现代码
public ListNode deleteDuplication(ListNode pHead) {
/* 传入空链表直接返回 */
if(pHead == null) {
return null;
}
/* 构建一个新的头节点 */
ListNode headNode = new ListNode(-1);
headNode.next = pHead;
/* 遍历指针 */
ListNode pList = pHead;
ListNode prevList = headNode;
/* 对该链表进行遍历,因为可能尾结点重复,此时删除尾结点后prevList.next会将null赋予pList,所以加入pList判空条件 */
while(pList != null && pList.next != null) {
if(pList.next.val == pList.val) {
/* 遇到重复节点后,遍历到最后一个重复节点 */
while(pList.next!=null && pList.next.val==pList.val) {
pList = pList.next;
}
prevList.next = pList.next;
pList = prevList.next;
} else {
prevList = pList;
pList = pList.next;
}
}
return headNode.next;
}
面试题05
问题描述
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
实现思路
简单想法
因为Java已经封装好了字符串的类型和对应方法,所以一种很简单的方法是直接返回,像这样:
public class Solution {
public String replaceSpace(StringBuffer str) {
return str.toString().replace(" ", "%20");
}
}
当然,这种方法太过于取巧,HR希望的看到的肯定是我们的思维而不是使用已经封装好的方法。
另一种方法是构造一个可变长字符串来作为存储容器,遍历原字符串,每次遇到空格就追加“%20”,像这样:
public class Solution {
public String replaceSpace(StringBuffer str) {
/* 构造可变长最终字符串 */
StringBuilder fstr = new StringBuilder();
/* 循环遍历目标字符串,将字符复制到最终字符串中,遇到空格则追加"%20" */
for(int i=0; i<str.length(); i++) {
if(str.charAt(i) == ' ') {
fstr.append("%20");
} else {
fstr.append(str.charAt(i));
}
}
return fstr.toString();
}
}
这个方法比上述方法正常了一点,但是还是有点取巧的意思,感觉没有触碰到该题的真正意义。
那么,一种简单直接所有人都能想到的思路是:从前往后遍历该字符串,遇到空格就把后面的字符串后移来空出位置,再把空格替换。
对于问题本身来说,字符串移动是必然的,所以一定要在解答前了解字符串是否已经给出足够的空间还是需要我们自己去扩容。
这样来看,对于从前往后的遍历方法,在最后的字符串往往需要移动很多次(前面每有一个空格被替换就需要往后移动一次),我为什么不能一次就把字符串移动到其对应的位置?
优化思路
答案当然是必然的,既然从前往后移动次数过于频繁,那么从后往前呢?将字符串从后往前遍历,一开始就把最后的字符串移动到它相应的位置上,这样前面空格就自动空出来了位置,此时所有字符串不就只需要移动就可以了?
也就是说,一开始先遍历字符串,得到字符串最终的长度作为第二个目标指针(空格是1个字符,"%20"是3个字符,所以每遍历到一个字符让长度加2)。
之后对字符串容量进行扩容,两个指针往前同时复制移动,遇到空格就替换,此时每遇到一个空格指针B就会往前多走一段距离。
因为指针A和B都指向了字符串长度,所以一开始A和B相差的其实就是空格没被替换时的距离,当空格被替换完毕后指针A和B重合,此时遍历结束。
实现代码
public class Solution {
/**
* 字符串中空格替换为"%20"
* @param str 字符串
* @return 替换后的字符串
*/
public String replaceSpace(StringBuffer str) {
/* 构建两个指针,前者指向原来数组最后长度位置,后者指向替换后的数组长度位置 */
int fPtr = str.length()-1;
int sPtr = fPtr;
/* 每遇到一个空格加2长度,最后sPtr指向的字符数组位置为替换后的长度位置 */
for(int i=0; i<=fPtr; i++) {
if(str.charAt(i) == ' ') {
sPtr += 2;
}
}
/* 为字符串扩容 */
str.setLength(sPtr+1);
/* 从后往前遍历字符串 */
while(fPtr != sPtr) {
/* 遇到空格就替换,否则两个指针一直往前遍历 */
if(str.charAt(fPtr) != ' ') {
str.setCharAt(sPtr, str.charAt(fPtr));
fPtr --;
sPtr --;
} else {
str.setCharAt(sPtr --, '0');
str.setCharAt(sPtr --, '2');
str.setCharAt(sPtr --, '%');
fPtr --;
}
}
return str.toString();
}
}
面试题06
问题描述
输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
实现思路
倒序输出很容易想到的一点就是构造一个栈结构,典型的后进先出的结构。
将链表中的值依次压入栈中,再从栈弹出到集合中。
而既然用栈可以完成,而递归本身就是一个栈结构,所以用递归同样可以实现(而且代码更加简洁)。
用递归实现的思路是:将下一节点作为参数递归,输出本身的值。
递归图解:
非递归代码
public class Solution {
static class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
/**
* 从尾到头打印链表
* @param listNode 链表头指针
* @return 从尾到头的值的集合
*/
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
/* 倒序链表值保存集合 */
ArrayList<Integer> fList = new ArrayList<>();
/* 构造一个空栈 */
Stack<Integer> stack = new Stack<>();
/* 将链表的值压入栈中 */
while(listNode != null) {
stack.push(listNode.val);
listNode = listNode.next;
}
/* 将栈中的值推入集合中 */
while(!stack.empty()) {
fList.add(stack.pop());
}
return fList;
}
}
递归代码
public class Solution {
static class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
/**
* 从尾到头打印链表
* @param listNode 链表头指针
* @return 从尾到头的值的集合
*/
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
/* 倒序链表值保存集合 */
ArrayList<Integer> fList;
/* 基准条件,递归到链表末尾返回一个空集合以添加值 */
if(listNode == null) {
return new ArrayList<>();
}
fList = printListFromTailToHead(listNode.next);
fList.add(listNode.val);
return fList;
}
}
面试题07
问题描述
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
实现思路
首先我们知道,对于前序数组中的值在对应的中序数组中,中序数组该值的左边是该值节点的左子树,右边是右子树。
而对于左子树与右子树来说,同样是遵循这个规律。
所以,我们可以把左右子树当作新的二叉树,左右子树的前、中序数组作为新的数组递归调用,直到调用到叶子节点。
具体的做法是:对于前序数组中的每一个值,都从中序数组中找到对应的值,并把该值左边的值作为新的二叉树作为入参递归调用,返回给该节点的左指针位置,右边的值做相同的操作。
实现代码
public class Solution {
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
/**
* 重建二叉树
* @param pre 前序遍历结果
* @param in 中序遍历结果
* @return 结果二叉树
*/
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
/* 得到前序和中序数组的初始临界 */
int strPre = 0, endPre = pre.length-1;
int strIn = 0, endIn = in.length-1;
/* 构建根节点 */
TreeNode root = reConstructBinaryTree(pre, strPre, endPre, in, strIn, endIn);
return root;
}
/**
* 重载“重建二叉树”方法,用数组的临界值控制数组遍历
* @param pre 前序遍历结果
* @param strPre 前序遍历数组初始值
* @param endPre 前序遍历数组最后值
* @param in 中序遍历结果
* @param strIn 中序遍历数组初始值
* @param endIn 中序遍历数组最后值
* @return 结果二叉树
*/
public TreeNode reConstructBinaryTree(int[] pre, int strPre, int endPre, int[] in, int strIn, int endIn) {
/* 临界情况,假如左(右)子树没有值,返回null */
if(strPre>endPre || strIn>endIn) {
return null;
}
/* 首先构建根节点的值 */
TreeNode root = new TreeNode(pre[strPre]);
/* 遍历中序数组,找到前序数组的值 */
for(int i=strIn; i<=endIn; i++) {
if(in[i] == pre[strPre]) {
root.left = reConstructBinaryTree(pre, strPre+1, strPre+i-strIn, in, strIn, i-1);
root.right = reConstructBinaryTree(pre, strPre+i-strIn+1, endPre, in, i+1, endIn);
}
}
return root;
}
}
Github
所有面试题的实现我都会放在我的Github仓库中,包括多种实现与详细注释,需要的可以去以下网址查看:
剑指Offer-Java实现