剑指offer刷题笔记
本文题解和图片来自CS-Notes
因为我在看题解时并不能一下就看懂,所以记录一下自己在做题和看题解时的思路过程
在原题解的基础上做了一定的修改
3. 数组中重复的数字
题目描述
在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
Input:
{2, 3, 1, 0, 2, 5}
Output:
2
审题:
在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。这说明数组的元素值与数组的长度有关,比如数组长为2,那么数组里的元素最小为0,最大为1。
解题思路
对于这种数组元素在[0,n-1]范围内的问题,可以将值为i的元素调整到第i个位置上进行求解。那怎么调整呢?
例如{2, 3, 1, 0, 2, 5}
第一次调整,遍历第一个元素2,判断元素值2是否与下标0相等,不是,则将它与以此元素值为下标的元素交换,也就是将arr[0]与arr[2]交换,即arr[i]与arr[arr[i]]交换,arr[arr[i]]指的就是以当前遍历元素的值为下标的元素。现在这个元素就是1
那么得到的是{1,3,2,0,2,5}
第二次调整,此时下标为1,注意:此时数组已经不是最初的数组,是上一次得到的数组;这次的元素是3,应该与下标为3的元素交换,即与0交换,{1,0,2,3,2,5}
第三次调整, 这次是2,下标也是2,所以不必交换
第四次调整,下标是3,值也为3,不必交换
第五次调整,下标是4,值为2,应该与下标为2的交换,但是此时我们知道下标为2的元素已经是2了,所以在这一步就可以得到重复的数字!
那怎么得到它呢,在上一个判断的基础上再进行判断,如果当前元素要交换到的位置上的元素与当前元素相等,那么它就是一个重复元素。代码表示为arr[i] == arr[arr[i]]。
public boolean duplicate(int numbers[],int length,int [] duplication) {
if(numbers == null || length == 0)
return false;
for(int i = 0;i<length;i++){
while(numbers[i] != i){
if(numbers[i] == numbers[numbers[i]]){
duplication[0] = numbers[i];
return true;
}
swap(numbers,i,numbers[i]);
}
}
return false;
}
private void swap(int[] arr,int i,int j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
值得注意的是,给出的方法返回值是布尔值而不是重复元素的值,需要返回的元素值传给了数组duplication,而且数组长度作为了一个单独的值传入。另外,把元素的交换放在了判断是否重复之后,减少了不必要的交换。
4.二维数组中的查找
题目描述
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
依照题意,此二维数组可以举例如下图:
解题思路
该二维数组中的一个数,小于它的数一定在其左边或上边,大于它的数一定在其右边或下边,为了缩小查找范围,我们从右上角或左下角开始找。为什么呢?如果从右上角开始找,那小于它的数一定在其左边,因为上边已经没有数字了,大于它的数一定在其下边,因为右边已经没有数字了,同理,左下角也是一样。
public boolean Find(int target, int [][] array) {
if(array == null || array.length == 0 || array[0].length == 0)
return false;
int rows = array.length,cols = array[0].length;
//行数为二维数组的长,列数为二维数组中一行的长,这里每一行长度都相等
int r = 0,c = cols - 1;
while(r <=rows - 1 && c >= 0){
if(array[r][c] > target)
//如果当前数大于目标值,就往左找
c--;
else if(array[r][c] < target)
//如果当前数小于目标值,就往下找
r++;
else
return true;
}
return false;
}
5.替换空格
题目描述
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
解题思路
先看简单的情况:
Input:
“A B”
Output:
“A%20B”
可以看到字符串的长度发生了变化,比之前多了两个字符的长度,于是先在尾部填充任意字符,使得字符串的长度等于替换后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。
令p1指向字符串原来的末尾位置,p2指向字符串填充字符后的末尾位置。p1和p2从后向前遍历,当p1遍历到非空格字符时,令p2指向的位置填充相同的字符,当p1遍历到空格字符时,就令p2指向的位置依次填充02%(因为是逆向遍历,所以是逆序填充)。
从后向前遍历是为了在改变p2所指向的内容时,不会影响到p1遍历原来字符串的内容。
public String replaceSpace(StringBuffer str) {
int p1 = str.length() - 1;
for(int i = 0; i <= p1; i++){
//对字符串遍历,当遇到空格就在末尾追加两个空格字符
if(str.charAt(i) == ' ')
str.append(" ");
}
int p2 = str.length() - 1;
//现在是追加后的字符串,令p2指向它的末尾
while(p1 >= 0 && p2 > p1){
char c = str.charAt(p1--);
if(c == ' '){
//如果p1遇到空格,p2就把当前指向的字符替换掉
str.setCharAt(p2--,'0');
str.setCharAt(p2--,'2');
str.setCharAt(p2--,'%');
}else{
str.setCharAt(p2--,c);
}
}
return str.toString();
}
6.从尾到头打印链表
题目描述
输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。
解题思路
使用递归
要逆序打印链表 1->2->3(3,2,1),可以先逆序打印链表 2->3(3,2),最后再打印第一个节点 1。而链表 2->3 可以看成一个新的链表,要逆序打印该链表可以继续使用求解函数,也就是在求解函数中调用自己,这就是递归函数。
//传入节点1
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
//新建一个list
ArrayList<Integer> ret = new ArrayList<>();
//每一次调用递归都创建一个list,用来存储下一个节点调用递归方法返回的list和当前节点的值
if (listNode != null) {
//下一个节点是2,2再调用方法,返回一个list,放入当前list
ret.addAll(printListFromTailToHead(listNode.next));
//存入该节点的值
ret.add(listNode.val);
}
return ret;
}
用addAll是因为这个递归方法返回的是一个list集合,而add传入的是一个int值
为什么不先存入当前节点值再存入下一个list呢?因为arraylist是存取有序的,存入顺序与取出顺序一致,而我们需要逆序打印链表,那么越靠前的元素就越在后面输出,例如1->2->3,先存入的应该是2->3这个链表,再存入1的值,这样取出的时候就是先取出2->3,再取出1。
另外,使用递归必须有一个基准条件和一个不断向基准条件推进的过程
这里的基准条件是listNode == null ,不断推进的过程是printListFromTailToHead(listNode.next)
使用头插法
使用头插法可以得到一个逆序的链表。
头结点和第一个节点的区别:
- 头结点是在头插法中使用的一个额外节点,这个节点不存储值;
- 第一个节点就是链表的第一个真正存储值的节点,称为首节点
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
// 头插法构建逆序链表
ListNode head = new ListNode(-1);
//创建头结点,值设为1
while (listNode != null) {
ListNode memo = listNode.next;
//保存传入节点的下一个节点,防止丢失
listNode.next = head.next;
//设置当前节点的下一个节点为首节点
head.next = listNode;
//把首节点设为当前节点,此时之前的首节点变为当前节点的下一个
listNode = memo;
//转到下一个节点继续操作
}
// 构建 ArrayList
ArrayList<Integer> ret = new ArrayList<>();
head = head.next;
while (head != null) {
ret.add(head.val);
head = head.next;
}
return ret;
}
在把首节点设为当前节点之前,需要把当前节点的下一个节点设为首节点,因为需要断开当前节点与旧链表的联系,与新链表连接。
使用栈
栈具有后进先出的特点,在遍历链表时将值按顺序放入栈中,最后出栈的顺序即为逆序。
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
Stack<Integer> stack = new Stack<>();
while (listNode != null) {
stack.add(listNode.val);
listNode = listNode.next;
}
ArrayList<Integer> ret = new ArrayList<>();
while (!stack.isEmpty())
ret.add(stack.pop());
return ret;
}
7.重建二叉树
题目描述
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
解题思路
1 找到根节点
2 划分左子树和右子树
前序遍历的结果中,第一个结点一定是根结点,然后在中序遍历的结果中查找这个根结点,根结点左边的就是左子树,根结点右边的就是右子树,递归构造出左、右子树即可
每棵子树的根节点肯定是pre子数组的首元素,所以每次新建一个子树的根节点
递归思想,每次将左右两颗子树当成新的子树进行处理,中序的左右子树索引很好找,前序的开始结束索引通过计算中序中左右子树的大小来计算,然后递归求解,直到startPre>endPre||startIn>endIn说明子树整理完毕。方法每次返回左子树或右子树的根节点
startpre+1,指的是前序遍历左子树的开始位置,即前序遍历根节点的下一个节点。i-startin,i指的是中序遍历根节点的位置,减去中序遍历开始位置即左子树的长度。所以i-startIn+starpre是前序遍历左子树结束的位置。
i-starin是在计算当前节点左子树节点个数;i-starin+starpre是确定当前节点左子树在pre里的位置;根据中序遍历+前序遍历的特点,pre[starpre+1]就是当前节点的左子节点,pre[i-starin+starpre+1]就是当前节点的右子节点;巧妙的是那个判定条件,越界即左右子节点不存在
根据题目给出的前序遍历、中序遍历数组,首先找出根节点,然后再根据中序遍历找到左子树和右子树的长度,分别构造出左右子树的前序遍历和中序遍历序列,最后分别对左右子树采取递归,递归跳出的条件是序列长度为1.
public class Solution {
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
TreeNode root=reConstructBinaryTree(pre,0,pre.length-1,in,0,in.length-1);
//重载方法,因为需要递归控制子数组的开始和结束索引
return root;
}
private TreeNode reConstructBinaryTree(int [] pre,int startPre,int endPre,int [] in,int startIn,int endIn) {
if(startPre>endPre||startIn>endIn)//递归终止条件
return null;
TreeNode root=new TreeNode(pre[startPre]);
//每次都把左右两棵子树当成新的子树处理
for(int i=startIn;i<=endIn;i++){
//在中序遍历数组中遍历查找根节点
if(in[i]==pre[startPre]){
//找到根节点在中序遍历的位置,那么左边就是左子树,右边就是右子树
root.left=reConstructBinaryTree(pre,startPre+1,startPre+i-startIn,in,startIn,i-1);
//startPre+1传入下一个要比较的先序遍历数组元素
// i-startIn i就是根节点的位置,减去中序遍历开始的位置,就是左子树的长度
//加上startPre,就和startPre+1构成一个先序遍历的子数组,里面包含的就是左子树的前序遍历序列
//startIn是中序遍历开始的位置,i-1是根节点前面一个,左子树的中序遍历序列
root.right=reConstructBinaryTree(pre,i-startIn+startPre+1,endPre,in,i+1,endIn);
//上面已经知道,i-startIn就是左子树的长度,加上startPre+1,那么这就是右子树的开始索引
//endPre是先序遍历的结束索引,那么这个子数组就是右子树的先序遍历序列
//i+1就是根节点后面一个,那么这个子数组就就是右子树的中序遍历序列
break;//如果在中序数组中找到了直接跳出循环
}
}
return root;
}
}