title: 剑指offer算法题
date: 2019-03-12 09:39:48
categories: 代码
tags: 算法学习
剑指offer算法题
数据结构
二维数组中的查找
题目描述
给定一个二维数组,其每一行从左到右递增排序,从上到下也是递增排序。给定一个数,判断这个数是否在该二维数组中。
Consider the following matrix:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
Given target = 5, return true.
Given target = 20, return false.
解题思路
要求时间复杂度 O(M + N),空间复杂度 O(1)。
该二维数组中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间,当前元素的查找区间为左下角的所有元素。
{% asset_img 二维数组.gif 二维数组 %}
public class Test03 {
/**
* 在一个二维数组中,每一行从左到右递增,每一列从上到下递增。
* 完成一个函数,输入一个这样的二维数组和一个整数,判断数组中是否含有该整数
*
* 解题思路: 首先选取右上角的数字,如果右上角的数字等于要查找的数字,结束。
* 如果该数字大于要查找的数字,剔除该数字所在列,如果该数字小于要查找的数字,剔除该数字所在行
* 该方法每一次都剔除一行或一列。进一步缩小范围
*
* @param matrix 二维数组
* @param number 要查找的
* @return 查找结果 true找到,false没有找到
*/
public static boolean find(int[][] matrix, int number) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return false;
}
int rows = matrix.length; //数组行数
int cols = matrix[0].length; //数组行的列数
//确保从右上角开始
int r=0;
int c =cols-1;
//要查找的位置确保在数组内
while (r <= rows - 1 && c >= 0) {
if (number == matrix[r][c]) {
return true;
} else if (number > matrix[r][c]) {
r++; //行数加一表示向下移动
} else {
c--; //列数减一表示向左移动
}
}
return false;
}
/**
* 测试数据
* @param args
*/
public static void main(String[] args){
int[][] matrix = {
{1, 2, 8, 9},
{2, 4, 9, 12},
{4, 7, 10, 13},
{6, 8, 11, 15}
};
System.out.println(find(matrix, 7)); // 要查找的数在数组中
System.out.println(find(matrix, 5)); // 要查找的数不在数组中
System.out.println(find(matrix, 1)); // 要查找的数是数组中最小的数字
System.out.println(find(matrix, 15)); // 要查找的数是数组中最大的数字
System.out.println(find(matrix, 0)); // 要查找的数比数组中最小的数字还小
System.out.println(find(matrix, 16)); // 要查找的数比数组中最大的数字还大
System.out.println(find(null, 16)); // 健壮性测试,输入空指针
}
}
替换空格
题目描述
将一个字符串中的空格替换成 “%20”。
Input:
"A B"
Output:
"A%20B"
解题思路
{% asset_img 替换过程.png 替换过程 %}
在字符串尾部填充任意字符,使得字符串的长度等于替换之后的长度。因为一个空格要替换成三个字符,因此当遍历到一个空格时,需要在尾部填充两个任意字符。
令 P1 指向字符串原来的末尾位置,P2 指向字符串现在的末尾位置。P1 和 P2 从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。
从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。
{% asset_img 替换空格.gif 替换空格 %}
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;
//p1与p2重合时循环结束
while (p1 >= 0 && p2 > p1) {
char c = str.charAt(p1--);
if (c == ' ') {
str.setCharAt(p2--, '0');
str.setCharAt(p2--, '2');
str.setCharAt(p2--, '%');
} else {
//如果c不等于空格,则将c向后移动到p2的位置
str.setCharAt(p2--, c);
}
}
return str.toString();
}
从尾到头打印链表
问题描述
从尾到头反过来打印链表的值
{% asset_img 倒序打印链表.gif 倒序打印链表 %}
解题思路
{% asset_img 使用递归.gif 使用递归 %}
使用递归
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> ret = new ArrayList<>();
if (listNode != null) {
ret.addAll(printListFromTailToHead(listNode.next));
ret.add(listNode.val);
}
return ret;
}
使用头插法
利用链表头插法为逆序的特点。
头结点和第一个节点的区别:
- 头结点是在头插法中使用的一个额外节点,这个节点不存储值;
- 第一个节点就是链表的第一个真正存储值的节点。
{% asset_img 头插法.gif 头插法 %}
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
// 头插法构建逆序链表
ListNode head = new ListNode(-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;
}
使用栈
{% asset_img 使用栈.gif 栈 %}
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
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;
}
重建二叉树
题目描述
根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
preorder = [3,9,20,15,7]
inorder = [9,3,15,20,7]
{% asset_img 重建二叉树.gif 重建二叉树 %}
解题思路
前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。
{% asset_img 二叉树.gif 二叉树 %}
方法1:
/**
* Definition for binary tree
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class CB1{
/**
* 重建二叉树
*
* @param pre 先序序列
* @param in 中序序列
* @return 二叉树根结点
*/
public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
if (pre == null || in == null || pre.length != in.length) {
return null;
}
int n = pre.length;
return constructBinaryTree(pre, 0, n - 1, in, 0, n - 1);
}
private TreeNode constructBinaryTree(int[] pre, int startPre, int endPre, int[] in, int startIn, int endIn) {
TreeNode node = new TreeNode(pre[startPre]);
if (startPre == endPre) {
if (startIn == endIn) {
return node;
}
throw new IllegalArgumentException("Invalid input!");
}
int inOrder = startIn;
while (in[inOrder] != pre[startPre]) {
++inOrder;
if (inOrder > endIn) {
new IllegalArgumentException("Invalid input!");
}
}
int len = inOrder - startIn;
if (len > 0) {
// 递归构建左子树
node.left = constructBinaryTree(pre, startPre + 1, startPre + len, in, startIn, inOrder - 1);
}
if (inOrder < endIn) {
// 递归构建右子树
node.right = constructBinaryTree(pre, startPre + len + 1, endPre, in, inOrder + 1, endIn);
}
return node;
}
}
用两个栈实现队列
问题描述
题目: 用两个栈实现一个队列。请实现它的两个函数,appendTail和deleteHead,分别完成在队列尾部和插入节点和在队列头部删除节点的功能。
即实现push和pop操作。
解题思路
stack1 栈用来处理入栈(push)操作,stack2 栈用来处理出栈(pop)操作。一个元素进入 stack1 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 stack2 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序。
{% asset_img 队列.gif %}
{% asset_img 实现队列.png %}
Stack<Integer> in = new Stack<Integer>();
Stack<Integer> out = new Stack<Integer>();
public void push(int node) {
in.push(node);
}
public int pop() throws Exception {
if (out.isEmpty())
while (!in.isEmpty())
out.push(in.pop());
if (out.isEmpty())
throw new Exception("queue is empty");
return out.pop();
}
排序和查找
旋转数组的最小数字
问题描述
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。
解题思路
在一个有序数组中查找一个元素可以用二分查找,二分查找也称为折半查找,每次将查找区间减半,这种这般特性的算法复杂度都为O(logN)。
本题可以修改二分查找算法进行求解:
- 当 nums[m] <= nums[h] 的情况下,说明解在 [l, m] 之间,此时令 h = m;
- 否则解在 [m + 1, h] 之间,令 l = m + 1。
{% asset_img 二分.png %}
public int min(int[] nums){
if (nums.length == 0){
return 0;
}
int l = 0, h = nums.length - 1;
while (l < h) {
int m = 1 + (h - 1) / 2;
if (nums[m] <= nums[h]) {
h = m;
} else {
l = m+1;
}
}
return nums[l];
}
如果允许数组元素重复的话,那么会出现特殊的情况:nums[l] == nums[m] == nums[h],那么此时无法确定解在哪个区间,需要切换到顺序查找。例如对于数组 {1,1,1,0,1},l、m 和 h 指向的数都为 1,此时无法知道最小数字 0 在哪个区间。
斐波那契数列
题目描述
求斐波那契数列的第 n 项,n <= 39。
{% asset_img fib.jpg %}
解题思路
若使用递归则会重复计算一些子问题,如f(10)需要计算f(9)和f(8),计算f(9)需要计算f(8)和f(7),可以看到f(8)被重复计算。
{% asset_img fib.gif %}
递归是将一个问题划分为多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,从而避免重复求解子问题。
public int Fibonacci(int n){
if (n <= 1) {
return n;
}
int[] fib = new int[n + 1];
fib[1] = 1;
for (int i = 2; i<=n; i++){
fib[i] = fib[i -1] + fib[i -2];
}
return fib[n];
}
考虑到第 i 项只与第 i-1 和第 i-2 项有关,因此只需要存储前两项的值就能求解第 i 项,从而将空间复杂度由 O(N) 降低为 O(1)。
public int Fibonacci2(int n){
if (n <= 1) {
return n;
}
int pre2 = 0, pre1 = 1;
int fib = 0;
for (int i =2; i<=n;i++){
fib = pre2 + pre1;
pre2 = pre1;
pre1 = fib;
}
return fib;
}
由于待求解的 n 小于 40,因此可以将前 40 项的结果先进行计算,之后就能以 O(1) 时间复杂度得到第 n 项的值了。
public class Solution {
private int[] fib = new int[40];
public Solution() {
fib[1] = 1;
for (int i = 2; i < fib.length; i++)
fib[i] = fib[i - 1] + fib[i - 2];
}
public int Fibonacci(int n) {
return fib[n];
}
}
矩形覆盖
题目描述
我们可以用 2 1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个 2 1的小矩形无重叠地覆盖一个2 * n的大矩形,总共有多少种方法。
{% asset_img 矩形.gif %}
解题思路
public int RectCover(int n){
if(n <= 2)
return n;
int pre2 = 1, pre1 = 2;
int result = 0;
for (int i = 3; i <= n; i++){
result = pre2 + pre1;
pre2 = pre1;
pre1 = result;
}
return result;
}
跳台阶
题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级台阶总共有多少种跳法。
{% asset_img 蛙.png %}
解题思路
public int JumpFloor(int n) {
if (n <= 2)
return n;
int pre2 = 1, pre1 = 2;
int result = 1;
for (int i = 2; i < n; i++) {
result = pre2 + pre1;
pre2 = pre1;
pre1 = result;
}
return result;
}
变态跳台阶
题目描述
一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级… 它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
{% asset_img 蛙2.png %}
解题思路
动态规划
public int JumpFloorII(int target) {
int[] dp = new int[target];
Arrays.fill(dp, 1);
for (int i = 1; i < target; i++)
for (int j = 0; j < i; j++)
dp[i] += dp[j];
return dp[target - 1];
}
数学推导
跳上 n-1 级台阶,可以从 n-2 级跳 1 级上去,也可以从 n-3 级跳 2 级上去…,那么
f(n-1) = f(n-2) + f(n-3) + ... + f(0)
同样,跳上 n 级台阶,可以从 n-1 级跳 1 级上去,也可以从 n-2 级跳 2 级上去… ,那么
f(n) = f(n-1) + f(n-2) + ... + f(0)
综上可得
f(n) - f(n-1) = f(n-1)
即
f(n) = 2*f(n-1)
所以 f(n) 是一个等比数列
public int JumpFloorII(int target) {
return (int) Math.pow(2, target - 1);
}
二进制中1的个数
题目描述
输入一个整数,输出该数二进制表示中1的个数。
n&(n-1)
该位运算除n的位级表示中最低的那一位
n : 10110100
n-1 : 10110011
n&(n-1) : 10110000
时间复杂度O(M),其中M表示1的个数
public int NumberOf1(int n){
int cnt = 0;
while(n != 0){
cnt++;
n &= (n - 1);
}
return cnt;
}
数值的整数次方
题目描述
给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent,求 base 的 exponent 次方。
解题思路
下面的讨论中 x 代表 base,n 代表 exponent。
{% asset_img power.jpg %}
因为 (x*x)n/2 可以通过递归求解,并且每次递归 n 都减小一半,因此整个算法的时间复杂度为 O(logN)。
代码实现
方法1
public double Power(double base, int exponent) {
if (exponent == 0)
return 1;
if (exponent == 1)
return base;
boolean isNegative = false;
if (exponent < 0) {
exponent = -exponent;
isNegative = true;
}
double pow = Power(base * base, exponent / 2);
if (exponent % 2 != 0)
pow = pow * base;
return isNegative ? 1 / pow : pow;
}
方法2
/**
* 实现函数double Power(double base, int exponent) 求base的exponent次方
* 不使用库函数,且不用考虑最大值问题
* @param base 指次
* @param exponent 幂
*/
public static double power(double base, int exponent) {
//指数和幂不能同时为0
if (base == 0 && exponent == 0) {
throw new RuntimeException("invalid input. base and exponent both are zero");
}
if (exponent == 0) {
return 1;
}
//求指数的绝对值
long exp = exponent;
if (exponent < 0) {
exp = -exp;
}
double result = powerWithUnsignedExponent(base, exp);
//如果指数为负数,求倒数
if (exponent < 0){
result = 1 / result;
}
return result;
}
/**
* 求一个数的整数次幂,不考虑溢出
* @param base 指次
* @param exponent 幂
* @return 结果
*/
public static double powerWithUnsignedExponent(double base, long exponent) {
// 如果指数为0,返回1
if (exponent == 0) {
return 1;
}
// 指数为1,返回底数
if (exponent == 1) {
return base;
}
//递归求值的一半
double result = powerWithUnsignedExponent(base, exponent >> 2);
//求最终的值,如果是基数还要乘一次底数
result *= result;
if (exponent % 2 != 0) {
result *=base;
}
return result;
}
public static void main(String[] args) {
System.out.println(0.0000000000000000000000001111 == 0);
System.out.println(0.0000000000000000000000000000 == 0);
System.out.println(power(2, -4));
System.out.println(power(2, 4));
System.out.println(power(2, 0));
System.out.println(power(0.00000000000000000000000000001, -1));
System.out.println(power(0.00000000000000000000000000001, 1));
System.out.println(power(0.00000000000000000000000000001, 0));
System.out.println(power(0.00000000000000000000000000000, 0));
}
打印从 1 到最大的 n 位数
题目描述
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。
解题思路
由于 n 可能会非常大,因此不能直接用 int 表示数字,而是用 char 数组进行存储。
使用回溯法得到所有的数。
public void print1ToMaxOfNDigits(int n) {
if (n <= 0) {
return;
}
char[] number = new char[n];
print1ToMaxOfNDigits(number, 0);
}
private void print1ToMaxOfNDigits(char[] number, int digit) {
if (digit == number.length) {
printNumber(number);
return;
}
for (int i = 0; i < 10; i++) {
number[digit] = (char) (1 + '0');
print1ToMaxOfNDigits(number, digit + 1);
}
}
private void printNumber(char[] number) {
int index = 0;
while (index < number.length && number[index] == '0') {
index++;
}
while (index < number.length) {
System.out.print(number[index++]);
}
System.out.println();
}
在O(1)时间内删除链表节点
解题思路
①如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,然后令该节点指向下下个节点,再删除下一个节点,时间复杂度为O(1)。
{% asset_img 节点.png %}
②如果链表只有一个节点,那么直接删除节点
③否则就需要先便利列表,找到节点的前一个节点,然后让前一个节点指向null,时间复杂度为O(n).
{% asset_img 节点1.png %}
综上,如果进行N次操作,那么大约需要操作节点的次数为N-1+N=2N-1,其中N-1表示N-1个不是尾节点的每个节点以O(1)的时间复杂度操作节点的总次数,N表示一个尾节点以O(N)的时间复杂度操作节点的总次数,(2N-1)/N~2,因此该算法的平均时间复杂度为O(1).
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
public ListNode deleteNode(ListNode head, ListNode toDelete) {
if (head == null || toDelete == null) {
return null;
}
if (toDelete.next != null){
//要删除的节点不是尾节点
ListNode next = toDelete.next;
toDelete.val = next.val;
toDelete.next = next.next;
}
else {
if (head == toDelete)
//只有一个节点
head = null;
else {
ListNode cur = head;
while (cur.next != toDelete)
cur = cur.next;
cur.next = null;
}
}
return head;
}
调整数组顺序使奇数位于偶数前面
题目描述
需要保证奇数和奇数,偶数和偶数之间的相对位置不变,这和书本不太一样。
{% asset_img 奇偶.png %}
解题思路
public void reOrderArray(int[] nums){
int oddCnt = 0;
for (int val : nums) {
if (val % 2 ==1)
oddCnt++;
}
int[] copy = nums.clone();
int i = 0,j = oddCnt;
for (int num: copy){
if (num % 2 == 1) {
nums[i++] = num;
}
else {
nums[j++] = num;
}
}
}
链表中倒数第k个节点
解题思路
设链表的长度为N,设两个指针P1和P2,先让P1移动k个节点,则还有N-K和节点可移动。此时让P1和P2同时移动,可以知道当P1移动到链表结尾时,P2移动到N-K个节点处,该位置就是倒数第K个节点。
{% asset_img 倒数K.png %}
public ListNode FindKthToTail(ListNode head, int k){
if (head == null){
return null;
}
ListNode P1 = head;
while (P1 != null && k-->0){
P1 = P1.next;
}
if (k > 0)
return null;
ListNode P2 = head;
while (P1 != null) {
P1 = P1.next;
P2 = P2.next;
}
return P2;
}
反转链表
题目描述
{% asset_img reverse.png %}
解题思路
c实现
待
java实现:递归
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
public ListNode ReverseList(ListNode head){
if(head == null || head.next == null)
return head;
ListNode next = head.next;
head.next = null;
ListNode newHead = ReverseList(next);
next.next = head;
return newHead;
}
java实现:迭代
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
public ListNode ReverseList(ListNode head){
ListNode newList = new ListNode(-1);
while (head != null){
ListNode next = head.next; //将头结点的下一个节点储存在next中
head.next = newList.next;//(头结点情况)将头结点的下一个节点储存为空值。(一般情况)实现头尾反转
newList.next = head; //存储头结点的值
head = next; //头结点向前
}
return newList.next;
}