z
-
剑指offer所有题目详解,可访问我的github项目:KongJetLin-offer
-
目录
- Number1:二维数组查找
- Number2:替换空格
- Number3:从尾到头打印链表
- Number4:重建二叉树
- Number5:用2个栈实现队列
题目1 二维数组查找
题目描述:
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
分析:如下图,我们知道,只能从左下角或者从右上角入手,输入的数大于当前数就往右走,小于当前的数就往上走。
我们从左上角开始遍历,代码如下:
public static boolean Find(int target, int [][] array)
{
//初始化左下角坐标
int rowIndex = array.length-1;
int colIndex = 0;
/**
需要注意的是,由于colIndex是在增加,而rowIndex是在减少,因此循环判断的时候,要限制的是
colIndex<总列数,rowIndex>=0,这样才能限制角标不越界!!!不要限制错误!
*/
while(colIndex<array[0].length && rowIndex>=0)
{
if(target == array[rowIndex][colIndex])
return true;
else if(target > array[rowIndex][colIndex])
colIndex++;
else
rowIndex--;
}
return false;//循环后没有找到则返回false
}
此解法的时间复杂度为:O(m+n),其中m,n分别代表行数与列数。
题目2 替换空格
题目描述:
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
题目给出的输入字符串是StringBuffer类型的,这点要注意!
首先,对一个问题进行说明:
public static void main(String[] args)
{
/**
* 如下,对于StringBuffer的replace方法: replace(int start, int end, String str)
* 这个方法可以将单个字符的替换为多个字符,如此处将下标为5的空格" "(一个字符)替换为"%20"(3个字符),
* 我们发现,单个替换为多个字符后,后面的字符会自动后移。
*/
//创建字符串缓冲区对象
StringBuffer sb = new StringBuffer();
//添加数据
sb.append("hello ").append("world");
System.out.println("sb:"+sb);//sb:hello world
System.out.println(sb.length());//11
//替换
sb.replace(5, 6, "%20");
System.out.println("sb:"+sb);//sb:hello%20world
System.out.println(sb.length());//13
//------------------------------
/**
* 对于String,它的replace方法:replace(char oldChar, char newChar)
* 也可以将单个字符的替换为多个字符,如下,将空格" "(一个字符)替换为"%20"(3个字符),
* 我们发现,单个替换为多个字符后,后面的字符会自动后移。
*/
String str = "hello world";
System.out.println(str);//hello world
System.out.println(str.length());//11
String newStr = str.replace(" ", "%20");
System.out.println(newStr);//hello%20world
System.out.println(newStr.length());//13
/*
总结:String与StringBuffer的replace方法都可以实现将 较短的字符串 替换为 较长的字符串,
只不过他们一个是通过索引(StringBuffer),一个是通过字符寻找(String)。
且替换后,String与StringBuffer的长度都会自动扩大。
*/
}
方法1:直接在StringBuffer上进行添加,不添加新的数组
现在我们考虑怎么做替换操作。最直观的做法是从头到尾扫描字符串,每一次碰到空格字符的时候做替换。由于是把1个字符替换成3个字符,我们必须要把空格后面所有的字符都后移两个字节,否则就有两个字符被覆盖了。
举个例子,我们从头到尾把"We are happy.“中的每一个空格替换成”%20"。为了形象起见,我们可以用一个表格来表示字符串,表格中的每个格子表示一个字符.
假设字符串的长度是n,替换一个空格,移动后面的字符所需要的时间复杂度为:O(n)。如果有m个空格字符,那么替换所有空格的总的时间效率是O(n*m)。
public static String replaceSpace(StringBuffer str)
{
//首先寻找第一个" "的下标,如果没有找到,会返回-1
int index = str.indexOf(" ");
while(index != -1)
{
//如果找到空格,进行替换。
// 根据上面对StringBuffer的分析,可以将一个空格字符" "替换为3个字符长度的"%20",后面的字符会自动后移。
str.replace(index , index+1 , "%20");
/*
我看到有人是从index开始寻找空格,既 index = str.indexOf(" ",index);
其实不需要这样做,因为前面第一个空格已经被替换,那么我们此时寻找当前第一个空格即可,
这第一个空格就是之前的第二个空格。
*/
index = str.indexOf(" ");//继续寻找空格
/*
这个方法:运行时间:22ms,占用内存:9420k
*/
}
return str.toString();
}
方法2:创建一个新的StringBuffer进行替换
我们创建一个新的StringBuffer,然后遍历之前的StringBuffer,将前一共StringBuffer的字符添加到新的StringBuffer中。如果遍历到空格,新的StringBuffer添加"%20"。
public static String replaceSpace2(StringBuffer str)
{
StringBuffer newSb = new StringBuffer();
for (int i = 0; i < str.length(); i++)
{
char ch = str.charAt(i);
if(ch == ' ')
{
newSb.append("%20");
}
else
{
newSb.append(ch);
}
}
return newSb.toString();
//运行时间:20ms,占用内存:9400k
}
这个方法只需要遍历原来的StringBuffer即可,时间复杂度为O(n)。
方法3:创建一个新的数组进行替换
前面方法2是遇到空格就给新的StringBuffer添加"%20",因为StringBuffer不需要初始化长度。我们这里使用数组,数组必须初始化长度,那么我们必须先计算原来StringBuffer中空格的个数,依据个数计算数组长度来创建数组。
public static String replaceSpace3(StringBuffer str)
{
int length = str.length();//获取原来的StringBuffer的长度
int spaceNum = 0;//用于保存空格个数
//遍历获取原来StringBuffer中空格的个数
for (int i = 0; i < length ; i++)
{
if(str.charAt(i) == ' ')
spaceNum++;
}
int arrlen = spaceNum*3+(length-spaceNum);//计算新数组的长度
char[] arr = new char[arrlen];
//遍历原来的StringBuffer,从尾到头遍历,这样就不需要创建新的变量,可以直接利用length与arrLen。
while(length>0)
{
//如果遍历到的字符不是空格,将其直接添加到数组(注意添加后数组角标先减一再赋值)
if(str.charAt(length-1) != ' ')
{//注意,数组下标从arrlen-1开始,因此这里arrlen必须先减一再进行赋值,否则会报错
arr[--arrlen] = str.charAt(length-1);
}
else
{
//如果是空格,向数组添加"%20"3个字符(注意添加后数组角标应该减一再赋值)
//注意,由于是从后向前赋值,应该先复制0,再是2,再是%
arr[--arrlen] = '0';
arr[--arrlen] = '2';
arr[--arrlen] = '%';
}
length--;//遍历一次记得将length减1,以便取到下一个字符
}
return new String(arr);//这里不能返回toSting,否则打印数组地址,而应该返回利用数组构造一个新的字符串
//运行时间:20ms, 占用内存:9528k
}
同样,这种方法的时间复杂度是O(n)。使用数组的效率并没有比直接创建一个新的StringBuffer进行替换高。
方法4:使用String的replaceAll方法
这种方法最简单,但是效率并没有那么高。底层应该也是将原来的StringBuffer转换为String,在装换为char数组,然后一个空格一个空格进行替换,复杂度是O(n*m)。
public static String replaceSpace4(StringBuffer str) {
/*使用String的replaceAll方法
replaceAll(String regex, String replacement)
使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
*/
return str.toString().replaceAll(" ","%20");
//运行时间:23ms,占用内存:9368k
}
总结:方法2利用StringBuffer的特性较为简单,但是这个是java独有的,对于c++以及其他语言,使用方法1与方法4的时间复杂度都是O(n*m)级别,而使用第3种创建新数组的方式,时间复杂度是O(n)。
题目3 从尾到头打印链表
题目描述:输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
注意:ArrayList的add()方法是添加到ArrayList的尾部(ArrayList是数组实现,从尾部添加时间复杂度为 O(1) 。另外,ArrayList有下标,可以直接使用下标访问)。 因此,我们想使得ArrayList存储的是 链表从尾到头的顺序 的值,必须从链表的尾部开始add()进ArrayList。
方法1:递归
递归的时间复杂度是O(n),代码如下:
//递归方法(分为3步骤:求解最基本问题,求解当前问题的下一个较小问题,将较小问题的解整合为当前较大问题的解)
//方法涵义:将以listNode开头的链表的元素从尾到头赋值给ArrayList
public ArrayList<Integer> printListFromTailToHead1(ListNode listNode) {
ArrayList<Integer> arrayList = new ArrayList<>();
//1、最基本问题:当遍历到链表头结点是null,不给ArrayList添加,直接返回空的ArrayList
if(listNode == null)
return arrayList;
//如果链表头结点不是null
//求解较小的问题:较小链表将其所有元素从尾到头赋值给ArrayList,返回这个ArrayList
arrayList = printListFromTailToHead1(listNode.next);
//将较小问题的解整合为当前较大问题的解:监听当前较大链表的头结点的元素添加到ArrayList的尾部
arrayList.add(listNode.val);
return arrayList;//返回添加完毕的ArrayList
//运行时间:20ms,占用内存:8988k
}
方法2:利用栈的方法
使用一个栈,将链表从头到尾的元素存储到栈中,在将栈的元素存储到ArrayList中。但是这种方法会多使用一个栈的存储空间。时间复杂度依然是O(n)
//使用栈
public ArrayList<Integer> printListFromTailToHead2(ListNode listNode)
{
ArrayList<Integer> arrayList = new ArrayList<Integer>();
Stack<Integer> stack = new Stack<Integer>();
while(listNode != null)
{
stack.push(listNode.val);
listNode = listNode.next;
}
while(!stack.isEmpty())
{
arrayList.add(stack.pop());
}
return arrayList;
//运行时间:18ms,占用内存:9148k
}
当然我们也可以使用数组存储,不过这种方法与使用栈存储差不多,不再赘述。
题目4 重建二叉树
题目描述:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
其实题目要求就是:根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
分析:如下图,前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。然后根据中序遍历的分割结果,我们就可以知道当前根结点左子树的结点数目和右子树的结点数目,通过中序遍历算出当前结点左右子树的结点数目,然后在前序遍历中根据左右子树结点数目,确定左右子树结点的范围,这样就可以在前序遍历中找出找出左右子树的根结点。
在左右子树中,同样根据其根结点,在中序遍历中将结果分成两部分,同样算出左右子树的结点数目,然后在前序遍历中确定左右子树的范围…
注意!这里从前序遍历中取出结点用于构建二叉树;中序遍历用于分割确定左右子树的结点数目!
- 注:此处参考 CYC2018大佬的分析。
代码如下:
HashMap<Integer , Integer> map = new HashMap();
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
if(pre == null || in == null)
return null;
for(int i=0 ; i<in.length ; i++)
map.put(in[i] , i);
return reconstruct(pre , 0 , pre.length-1 , 0);
}
private TreeNode reconstruct(int[] pre , int pre_left , int pre_right , int inLeft)
{
if(pre_left > pre_right)
return null;
TreeNode root = new TreeNode(pre[pre_left]);
int inIndex = map.get(root.val);
int inLeftLength = inIndex - inLeft;
root.left = reconstruct(pre , pre_left+1 , pre_left+inLeftLength , inLeft);
root.right = reconstruct(pre , pre_left+inLeftLength+1 , pre_right , inIndex+1);
return root;
}
题目5 用2个栈实现队列
题目描述:
用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
分析:入队列,我们就正常地讲元素放入第一个栈中。
出队列,我们先把第一个栈里的内容按顺序搬到第二个栈里,负负得正,这样再按顺序从第二个栈出栈的时候,就成了进入第一个栈时的队列顺序,相当于出队列。
代码如下:
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
public void push(int node) {
/*
入队列,元素正常进入第一个栈。
这里不需要判断队列是否已经满了,因为如果模拟队列满了,就是栈1满了,也会提示。
*/
stack1.push(node);
}
public int pop() {
int result = 0;
//出队列,首先如果栈2还有之前栈1转移过来的元素,直接将元素出栈
if(!stack2.isEmpty())
{
result = stack2.pop();
}
else
{//如果栈1,栈2都为空,那么在运行时出栈失败,抛出运行时异常。
if(stack1.isEmpty())
{
throw new RuntimeException("queue is empty");
}
//如果栈1不为空,将栈1所有的元素转到栈2
while(!stack1.isEmpty())
{
stack2.push(stack1.pop());
}
//此时栈2必然有元素,将出栈元素赋予result即可
result = stack2.pop();
}
return result;
}