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;}// 构建 ArrayListArrayList<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 : 10110100n-1 : 10110011n&(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) {//指数和幂不能同时为0if (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,返回1if (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;}
