剑指Offer 01-10
1. 二维数组中的查找
本题知识点:查找
数组
题目描述
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
(设行数为 m, 列数为 n)
public class Solution {
public boolean Find(int target, int [][] array) {
}
}
代码一
/**
* 暴力解答 T(n) = O(m*n)
* 两重循环遍历数组
*/
public class Solution {
public boolean Find(int target, int [][] array) {
for(int i=0; i<array.length; i++){
for(int j=0; j<array[i].length; j++){
if(target == array[i][j]){
return true;
}
}
}
return false;
}
}
代码二
/**
* 二分法 T(n) = O(m*logn)
* 由于数组每行都是有序,对每行采用二分查找
*/
public class Solution {
public boolean Find(int target, int [][] array) {
for(int i=0; i<array.length; i++){
int low = 0;
int high = array[i].length - 1;
while(low <= high){
int mid = (low + high)/2;
if(target > array[i][mid]){
low = mid + 1;
}else if(target < array[i][mid]){
high = mid -1;
}else{ // 找到 target
return true;
}
}
}
return false;
}
}
代码三
/**
* 由于数组由上到下,由左到右递增(每个一维数组的长度相同)
* 选取右上角元素或左下角元素为起始点
* 以右上角开始为例:初始值 row = 0, col = array[0].length - 1;
* if(target < array[row][col]) col--; //(col >= 0)
* if(target > array[row][col]) row++; //(row <= array.length - 1)
* 注意越界的终止条件,最终有 T(n) = O(m+n)
*/
public class Solution {
public boolean Find(int target, int [][] array) {
int row = 0;
int col = array[0].length - 1;
while(row <= array.length - 1 && col >= 0){
if(target > array[row][col]){
row++;
}else if(target < array[row][col]){
col--;
}else{
return true;
}
}
return false;
}
}
2. 替换空格
本题知识点:字符串
题目描述
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
public class Solution {
public String replaceSpace(StringBuffer str) {
}
}
代码一
/**
* 新建 StringBuffer 用于保存和拼接替换后的字符串结果
* 其中 StringBuffer/StringBuilder 自带有 length()/charAt() 方法,因此不必转换成字符串再操作
* 若某位字符为空格,则拼接"%20";否则,拼接原来字符。最后返回 buffer 对象拼接出的结果
*/
public class Solution {
public String replaceSpace(StringBuffer str) {
StringBuffer buffer = new StringBuffer();
for(int i=0; i<str.length(); i++){
char c = str.charAt(i);
if(c == ' '){
buffer.append("%20");
}else{
buffer.append(c);
}
}
return buffer.toString();
}
}
代码二
/**
* 最简单的使用 String 类的字符串替换方法 replace()
* String.replace(String target, String replacement)
*/
public class Solution {
public String replaceSpace(StringBuffer str) {
return str.toString().replace(" ","%20");
}
}
代码三
/**
* 使用 String 类的正则匹配字符串替换方法 replaceAll()
* String.replaceAll(String regex, String replacement)
*/
public class Solution {
public String replaceSpace(StringBuffer str) {
// 正则表达式 \s 匹配任何空白字符,包括空格、制表符、换页符等等。
// 经测试文本编辑器编写的Java文件的制表符等也会被匹配而转换,因此使用 \s 不太合理,此处单用空格符
return str.toString().replaceAll(" ","%20");
}
}
3. 从尾到头打印链表
本题知识点:链表
题目描述
输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。
/**
* public class ListNode {
* int val;
* ListNode next = null;
*
* ListNode(int val) {
* this.val = val;
* }
* }
*
*/
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
}
}
代码一
/**
* 按顺序摘取链表的结点,
* 采用头插法将结点插入到 ArrayList 中,最后得到倒序的结果
* 每次插入结点到列表都要将列表的元素后移一位,共插入 n 次(n个元素),故 T(n) = O(n^2)
*/
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list = new ArrayList<>();
while (listNode != null) {
// 新结点以头插法插入列表中
list.add(0, listNode.val);
listNode = listNode.next;
}
return list;
}
}
代码二
/**
* 此解答使用到栈先进后出的特点:
* 先按顺序摘取链表结点入栈,
* 从栈中弹出结点,加入列表,所得结果为倒序排列
* T(n) = O(n)
*/
import java.util.ArrayList;
import java.util.Stack;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list = new ArrayList<>();
Stack<ListNode> stack = new Stack<>();
// 按顺序摘取链表结点压入栈中
while (listNode != null) {
stack.push(listNode);
listNode = listNode.next;
}
// 从栈中弹出结点,加入列表
while (!stack.empty()){
list.add(stack.pop().val);
}
return list;
}
}
代码三
/**
* 递归调用使用到的方法栈先进后出的特点,
* 类似二叉树后序遍历的方式进行递归,获得倒序的结果
* T(n) = O(n)
*/
import java.util.ArrayList;
public class Solution {
ArrayList<Integer> list = new ArrayList<>();
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
if(listNode != null){
// 递归调用自身,同时减小问题规模(调用 next)
printListFromTailToHead(listNode.next);
// 等到所有递归开始返回时,加入结点
list.add(listNode.val);
}
return list;
}
}
4. 重建二叉树
本题知识点:树
题目描述
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
/**
* Definition for binary tree
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Solution {
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
}
}
思路解析
二叉树的遍历有如下特点:
遍历方式 | 简记 | |||
---|---|---|---|---|
先序遍历 | root |
preLeft | preRight | 根左右 |
中序遍历 | inLeft | root |
inRight | 左根右 |
后序遍历 | postLeft | postRight | root |
左右根 |
代码
/**
* 将先序序列看作 root + preLeft + preRight 三个部分
* 将中序序列看作 inLeft + root + inRight 三个部分
*
* 解题思路:二叉树根节点为初始先序序列的root,使用先序序列首元素拆分中序为左右两个部分,
* 并根据 inLeft / inRight 的分别元素长度,拆分出 preLeft + preRight 序列 => 递归求解
*
* 返回条件:任意子树序列的长度(preLeft || preRight || inLeft || inRight)等于 0
*/
import java.util.Arrays;
public class Solution {
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
// 出口条件:子树序列长度为 0
if(pre.length == 0 || in.length == 0){
return null;
}
// 取先序序列的首元素为二叉树(包括其子树)的根结点 root
TreeNode node = new TreeNode(pre[0]);
for(int i=0; i<in.length; i++){
if(pre[0] == in[i]){ // 找到 root 在中序序列中的位置
// 此处关注中序的长度反求先序的长度:
// inLeft.length = (i)-(0) = preLeft.length = (i+1)-(1)
node.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i+1),Arrays.copyOfRange(in, 0, i));
// 同理,
// inRight.length = (in.length)-(i+1) = preLeft = (pre.length)-(i+1)
node.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i+1, pre.length),Arrays.copyOfRange(in, i+1, in.length));
}
}
return node;
}
}
/** * Arrays.copyOfRange() 摘自 JDK8,注意 【】 内的描述 * @param original the array from which a range is to be copied * @param from the initial index of the range to be copied, inclusive * @param to the final index of the range to be copied, 【exclusive】. * The length of the returned array will be 【 to - from 】. */ copyOfRange(T[] original, int from, int to);
5. 用两个栈实现队列
本题知识点:队列
栈
题目描述
用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
import java.util.Stack;
public class Solution {
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
public void push(int node) {
}
public int pop() {
}
}
思路分析
操作情况依次经历下述变化:
- 两栈都空
- -> stack1不空 stack2空
- -> stack1空 stack2不空
- -> 两栈都不空
两栈都空 | stack1=[] | stack2=[] | 操作步骤 |
---|---|---|---|
入栈 | stack1=[2, 1] | stack2=[] | 2 * stack1.push |
出栈 | stack1=[] | stack2=[] | return null |
1不空 2空 | stack1=[2, 1] | stack2=[] | 操作步骤 |
---|---|---|---|
入栈 | stack1=[4, 3, 2, 1] | stack2=[] | 2 * stack1.push |
出栈 | stack1=[] | stack2=[2, 3, 4] | 4 * stack1.pop 4 * stack2.push return stack2.pop |
1空 2不空 | stack1=[] | stack2=[2, 3, 4] | 操作步骤 |
---|---|---|---|
入栈 | stack1=[6, 5] | stack2=[2, 3, 4] | 2 * stack1.push |
出栈 | stack1=[] | stack2=[3, 4] | return stack2.pop |
两栈不空 | stack1=[6, 5] | stack2=[2, 3, 4] | 操作步骤 |
---|---|---|---|
入栈 | stack1=[8, 7, 6, 5] | stack2=[2, 3, 4] | 2 * stack1.push |
出栈 | stack1=[6, 5] | stack2=[3, 4] | return stack2.pop |
小结
- 入栈情况:总是 stack1.push
- 出栈情况:
- stack2 为空:将所有 stack1 元素入栈到 stack2,再执行 stack2.pop
- stack2 不空:stack2.pop
代码
import java.util.Stack;
public class Solution {
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
public void push(int node) {
stack1.push(node);
}
public int pop() {
// 若 stack2 为空:将所有 stack1 元素入栈到 stack2
if(stack2.empty()){
while(!stack1.empty()){
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
}
6. 旋转数组中的最小数字
本题知识点:队列
栈
题目描述
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] array) {
}
}
代码一
/**
* 原数组非减排序,最简单的想法是找到其数组的旋转中第一次出现减小的元素
* T(n) = O(n)
*/
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] array) {
// 数组长度为 0,返回 0
if(array.length == 0) return 0;
// 返回第一次出现减小元素的值
for(int i=0; i<array.length-1; i++){
if(array[i] > array[i+1]){
return array[i+1];
}
}
// 数组中所有元素都相等。返回以第一个元素的值
return array[0];
}
}
代码二
/**
* 出现查找的问题,怎么能少了二分查找呢!
* T(n) = O(logn)
* 每次循环排除有序部分,缩小查找区间(剩余一个/两个元素),最终找到分界元素
* 1. 只有一个元素时,会退出循环,返回 array[low]
* 2. 只有两个元素时,返回 array[low]
*/
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] array) {
if(array.length == 0) return 0;
int low = 0;
int high = array.length - 1;
int mid;
// 每次循环排除有序部分,缩小查找区间(剩余一个/两个元素),最终找到分界元素
while(low < high){ // 只有一个元素时,会退出循环,返回 array[low]
// 只有两个元素时,返回 array[low]
if (array[low] < array[high]) return array[low];
mid = (low + high)/2;
// 如果左半数组为有序数组
if(array[low] < array[mid]){
low = mid + 1;
}
// 如果右半数组为有序数组
else if(array[mid] < array[high]){
high = mid;
}
// 出现有相等情况
else{
low++;
}
}
return array[low];
}
}
7. 斐波那契数列
本题知识点:递归
题目描述
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。
n<=39
public class Solution {
public int Fibonacci(int n) {
}
}
代码一
/**
* 暴力递归法(性能超级差,谁试谁知道)
*/
public class Solution {
public int Fibonacci(int n) {
if(n == 0){
return 0;
}
if(n == 1){
return 1;
}
return Fibonacci(n-1)+Fibonacci(n-2);
}
}
代码二
/**
* 备忘录法 自顶而下,在树的多路归并时有较好的效果
*/
public class Solution {
int[] array = new int[40];
public int Fibonacci(int n) {
array[0] = 0;
array[1] = 1;
array[2] = 1;
if(n == 0){
return 0;
}
if(n == 1 || n == 2){
return 1;
}
int result = memo(n);
return result;
}
public int memo(int n){
if(array[n-2] == 0){
array[n-2] = memo(n-2);
}
if(array[n-1] == 0){
array[n-1] = memo(n-1);
}
return array[n-2] + array[n-1];
}
}
代码三
/**
* n 的值较小,因此可以直接将数组求出后,查找对应的 n 值
*/
public class Solution {
public int Fibonacci(int n) {
int[] array = new int[40];
array[0] = 0;
array[1] = 1;
for(int i=2; i<40; i++){
array[i] = array[i-1] + array[i-2];
}
return array[n];
}
}
代码四
/**
* 动态维护 i 和 j 两个值,根据 n 的值返回靠前的 i
*/
public class Solution {
public int Fibonacci(int n) {
int i = 0;
int j = 1;
int tmp;
while((n--)>0){
tmp = j;
j += i;
i = tmp;
}
return i;
}
}
8. 跳台阶
本题知识点:递归
题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
public class Solution {
public int JumpFloor(int target) {
}
}
分析
当 n = 1 时,只有一种跳法 f(1)=1
当 n = 2 时,有两种跳法 f(2)=2
当 n >= 3 时,由于第一次跳的情况只有两种 1步/2步,后序跳的对应的是 f(n-1)/f(n-2)
此时对于 f(n),有 f(n) = f(n-1)+f(n-2)
由上述分析可知该题所求是斐波那契数列。
代码一
/**
* 简单递归求解
*/
public class Solution {
public int JumpFloor(int target) {
if(target <= 0) return 0;
if(target == 1) return 1;
if(target == 2) return 2;
// 对于 target >= 3 的情况
return JumpFloor(target-1) + JumpFloor(target-2);
}
}
代码二
/**
* 分析知道了所求为斐波那契数列 f(n) = f(n-1) + f(n-2)
* 对递归求解的思路进行优化:
* 动态维护 i=f(n-2)+f(n-1) 和 j=f(n-1)+f(n) 两个值,最后返回 i
*/
public class Solution {
public int JumpFloor(int target) {
if(target <= 0) return 0;
int i = 1;
int j = 2;
int tmp;
/*
* 当 target = 1,不执行 while, 返回 i=1 {i=f(1)=1,j=f(2)=2}
* 当 target = 2,执行1遍 while,返回 i=2 {i=f(2)=2,j=f(1)+f(2)=3}
* 当 target = 3,执行2遍 while,返回 i=3 (i=f(1)+f(2)=3,j=f(2)+f(3)=5)
* ......
* 当 target = n,执行(n-1)遍 while,返回 i {i=f(n-2)+f(n-1),j=f(n-1)+f(n)}
*/
while((--target)>0){
tmp = j;
j += i;
i = tmp;
}
return i;
}
}
9. 跳台阶2
本题知识点:贪心
题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
public class Solution {
public int JumpFloorII(int target) {
}
}
代码
/**
* 对于最后一个台阶,必定是要被跳上的,
* 对与前 n 个台阶,每个都有跳上与不跳的跳法,因此跳法共有 2^(n-1) 种
*/
public class Solution {
public int JumpFloorII(int target) {
if(target <= 0) return 0;
return (int)Math.pow(2,target-1);
}
}