我的技术博客:www.zhenganwen.topjava
如下题目按照牛客网在线编程排序,全部代码示例代码均已经过牛客网OJ。node
在一个二维数组中(每一个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。面试
public boolean Find(int target, int [][] arr) {
}
复制代码
暴力方法是遍历一遍二维数组,找到target
就返回true
,时间复杂度为O(M * N)
(对于M行N列的二维数组)。编程
由题可知输入的数据样本具备高度规律性(单独一行的数据来看是有序的,单独一列的数据来看也是有序的),所以考虑可否有一个比较基准在一次的比较中根据有序性淘汰没必要再进行遍历比较的数。有序、查找,由此不难联想到二分查找,咱们能够借鉴二分查找的思路,每次选出一个数做为比较基准进而淘汰掉一些没必要比较的数。二分是选取数组的中位数做为比较基准的,所以可以保证每次都淘汰掉二分之一的数,那么此题中有没有这种特性的数呢?咱们不妨举例观察一下:数组
不难发现上图中对角线上的数是其所在行和所在列造成的序列的中位数,不妨选取右上角的数做为比较基准,若是不相等,那么咱们能够淘汰掉全部它左边的数或者它全部下面的,好比对于target = 6
,由于(0,3)
位置上的4 < 6
,所以(0,3)
位置及其同一行的左边的全部数都小于6所以能够直接淘汰掉,淘汰掉以后问题就变为了从剩下的三行中找target
,这与原始问题是类似的,也就是说每一次都选取右上角的数据为比较基准而后淘汰掉一行或一列,直到某一轮被选取的数就是target
或者已经淘汰得只剩下一个数的时候就必定能得出结果了,所以时间复杂度为被淘汰掉的行数和列数之和,即O(M + N)
,通过分析后不难写出以下代码:缓存
public boolean Find(int target, int [][] arr) {
//input check
if(arr == null || arr.length == 0 || arr[0] == null || arr[0].length == 0){
return false;
}
int i = 0, j = arr[0].length - 1;
while(i != arr.length - 1 && j != 0){
if(target > arr[i][j]){
i++;
}else if(target < arr[i][j]){
j--;
}else{
return true;
}
}
return target == arr[i][j];
}
复制代码
值得注意的是每次选取的数都是第一行最后一个数,所以前提是第一行有数,那么就对应着输入检查的arr[0] == null || arr[0].length == 0
,这点比较容易忽略。数据结构
总结:通过分析其实不难发现,此题是在一维有序数组使用二分查找元素的一个变种,咱们应该充分利用数据自己的规律性来寻找解题思路。app
请实现一个函数,将一个字符串中的每一个空格替换成“%20”。例如,当字符串为We Are Happy.则通过替换以后的字符串为We%20Are%20Happy。dom
public String replaceSpace(StringBuffer str) {
}
复制代码
此题考查的是字符串这个数据结构的数组实现(对应的还有链表实现)的相关操做。函数
若是可使用API
,那么能够很容易地写出以下代码:
public String replaceSpace(StringBuffer str) {
//input check
//null pointer
if(str == null){
return null;
}
//empty str or not exist blank
if(str.length() == 0 || str.indexOf(" ") == -1){
return str.toString();
}
for(int i = 0 ; i < str.length() ; i++){
if(str.charAt(i) == ' '){
str.replace(i, i + 1, "%20");
}
}
return str.toString();
}
复制代码
可是若是面试官告诉咱们不准使用封装好的替换函数,那么目的就是在考查咱们对字符串数组实现方式的相关操做。因为是连续空间存储,所以须要在建立实例时指定大小,因为每一个空格都使用%20
替换,所以替换以后的字符串应该比原串多出空格数 * 2
个长度,实现以下:
public String replaceSpace(StringBuffer str) {
//input check
//null pointer
if(str == null){
return null;
}
//empty str or not exist blank
if(str.length() == 0 || str.indexOf(" ") == -1){
return str.toString();
}
char[] source = str.toString().toCharArray();
int blankCount = 0;
for(int i = 0 ; i < source.length ; i++){
blankCount = (source[i] == ' ') ? blankCount + 1 : blankCount;
}
char[] dest = new char[source.length + blankCount * 2];
for(int i = source.length - 1, j = dest.length - 1 ; i >=0 && j >=0 ; i--, j--){
if(source[i] == ' '){
dest[j--] = '0';
dest[j--] = '2';
dest[j] = '%';
continue;
}else{
dest[j] = source[i];
}
}
return new String(dest);
}
复制代码
若是还要求不能有额外空间,那咱们就要考虑如何复用输入的字符串,若是咱们从前日后遇到空格就将空格及其以后的两个位置替换为%20
,势必会覆盖空格以后的两个字符,好比hello world
会被替换成hello%20rld
,所以咱们须要在长度被扩展后的新串中从后往前肯定每一个索引上的字符。好比使用一个originalIndex
指向原串中的最后一个字符索引,使用newIndex
指向新串的最后一个索引,每次将originalIndex
上的字符复制到newIndex
上而且两个指针前移,若是originalIndex
上的字符是空格,则将newIndex
依次填充0,2,%
,而后二者再前移,直到二者都到首索引位置。
public String replaceSpace(StringBuffer str) {
//input check
//null pointer
if(str == null){
return null;
}
//empty str or not exist blank
if(str.length() == 0 || str.indexOf(" ") == -1){
return str.toString();
}
int blankCount = 0;
for(int i = 0 ; i < str.length() ; i++){
blankCount = (str.charAt(i) == ' ') ? blankCount + 1 : blankCount;
}
int originalIndex = str.length() - 1, newIndex = str.length() - 1 + blankCount * 2;
str.setLength(newIndex + 1); //须要从新设置一下字符串的长度,不然会报越界错误
while(originalIndex >= 0 && newIndex >= 0){
if(str.charAt(originalIndex) == ' '){
str.setCharAt(newIndex--, '0');
str.setCharAt(newIndex--, '2');
str.setCharAt(newIndex, '%');
}else{
str.setCharAt(newIndex, str.charAt(originalIndex));
}
originalIndex--;
newIndex--;
}
return str.toString();
}
复制代码
总结:要把思惟打开,对于数组的操做咱们习惯性的以
for(int i = 0 ; i < arr.length ; i++)
的形式从头至尾来操做数组,可是不要忽略了从尾到头遍历也有它的独到之处。
输入一个链表,反转链表后,输出新链表的表头。
public ListNode ReverseList(ListNode head) {
}
复制代码
此题的难点在于没法经过一个单链表结点获取其前驱结点,所以咱们不只要在反转指针以前保存当前结点的前驱结点,还要保存当前结点的后继结点,并在下一次反转以前更新这两个指针。
/* 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 pre = null, p = head, next;
while(p != null){
next = p.next;
p.next = pre;
pre = p;
p = next;
}
return pre;
}
复制代码
输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
}
复制代码
此题的难点在于单链表只有指向后继结点的指针,所以咱们没法经过当前结点获取前驱结点,所以不要妄想先遍历一遍链表找到尾结点而后再依次从后往前打印。
因为咱们一般是从头至尾遍历链表的,而题目要求从尾到头打印结点,这与前进后出的逻辑是相符的,所以你可使用一个栈来保存遍历时走过的结点,再经过后进先出的特性实现从尾到头打印结点,可是咱们也能够利用递归来帮咱们压栈,因为递归简洁不易出错,所以面试中能用递归尽可能用递归:只要当前结点不为空,就递归遍历后继结点,当后继结点为空时,递归结束,在递归回溯时将“当前结点”依次添加到集合中
/** * 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<Integer> res = new ArrayList();
//input check
if(listNode == null){
return res;
}
recursively(res, listNode);
return res;
}
public void recursively(ArrayList<Integer> res, ListNode node){
//base case
if(node == null){
return;
}
//node not null
recursively(res, node.next);
res.add(node.val);
return;
}
}
复制代码
还有一种方法就是将链表指针都反转,这样将反转后的链表从头至尾打印就是结果了。须要注意的是咱们不该该在访问用户数据时更改存储数据的结构,所以最后要记得反转回来:
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> res = new ArrayList();
//input check
if(listNode == null){
return res;
}
return unrecursively(listNode);
}
public ArrayList<Integer> unrecursively(ListNode node){
ArrayList<Integer> res = new ArrayList<Integer>();
ListNode newHead = reverse(node);
ListNode p = newHead;
while(p != null){
res.add(p.val);
p = p.next;
}
reverse(newHead);
return res;
}
public ListNode reverse(ListNode node){
ListNode pre = null, cur = node, next;
while(cur != null){
//save predecessor
next = cur.next;
//reverse pointer
cur.next = pre;
//move to next
pre = cur;
cur = next;
}
//cur is null
return pre;
}
复制代码
总结:面试时能用递归就用递归,固然了若是面试官就是要考查你的指针功底那你也能
just so so
不是
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,2,7,1,5,3,8,6},则重建二叉树并返回。
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
}
复制代码
先序序列的特色是第一个数就是根结点然后是左子树的先序序列和右子树的先序序列,而中序序列的特色是先是左子树的中序序列,而后是根结点,最后是右子树的中序序列。所以咱们能够经过先序序列获得根结点,而后经过在中序序列中查找根结点的索引从而获得左子树和右子树的结点数。而后能够将两序列都一分为三,对于其中的根结点可以直接重建,而后根据对应子序列分别递归重建根结点的左子树和右子树。这是一个典型的将复杂问题划分红子问题分步解决的过程。
递归体的定义,如上图先序序列的左子树序列是2,3,4
对应下标1,2,3
,而中序序列的左子树序列是3,2,4
对应下标0,1,2
,所以递归体接收的参数除了保存两个序列的数组以外,还须要指明须要递归重建的子序列分别在两个数组中的索引范围:TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n)
。而后递归体根据pre
的i~j
索引范围造成的先序序列和in
的m~n
索引范围造成的中序序列重建一棵树并返回根结点。
首先根结点就是先序序列的第一个数,即pre[i]
,所以TreeNode root = new TreeNode(pre[i])
能够直接肯定,而后经过在in
的m~n
中查找出pre[i]
的索引index
能够求得左子树结点数leftNodes = index - m
,右子树结点数rightNodes = n - index
,若是左(右)子树结点数为0则代表左(右)子树为null
,不然经过root.left = rebuild(pre, i' ,j' ,in ,m' ,n')
来重建左(右)子树便可。
这个题的难点也就在这里,即i',j',m',n'
的值的肯定,笔者曾在此困惑许久,建议经过leftNodes,rightNodes
和i,j,m,n
来肯定:(这个时候了前往不要在脑子里面想这些下标对应关系!!必定要在纸上画,确保准确性和归纳性)
因而容易得出以下代码:
if(leftNodes == 0){
root.left = null;
}else{
root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1);
}
if(rightNodes == 0){
root.right = null;
}else{
root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n);
}
复制代码
笔者曾以中序序列的根节点索引来肯定i',j',m',n'
的对应关系写出以下错误代码:
if(leftNodes == 0){
root.left = null;
}else{
root.left = rebuild(pre, i + 1, index, in, m, index - 1);
}
if(rightNodes == 0){
root.right = null;
}else{
root.right = rebuild(pre, index + 1, j, in, index + 1, n);
}
复制代码
这种对应关系乍一看没错,可是不具备归纳性(即囊括全部状况),好比对序列2,3,4
、3,2,4
重建时:
你看这种状况,上述错误代码还适用吗?缘由就在于index
是在in
的m~n
中选取的,与数组in
是绑定的,和pre
没有直接的关系,所以若是用index
来表示i',j'
天然是不合理的。
此题的正确完整代码以下:
/** * 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) {
if(pre == null || in == null || pre.length == 0 || in.length == 0 || pre.length != in.length){
return null;
}
return rebuild(pre, 0, pre.length - 1, in, 0, in.length - 1);
}
public TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n){
int rootVal = pre[i], index = findIndex(rootVal, in, m, n);
if(index < 0){
return null;
}
int leftNodes = index - m, rightNodes = n - index;
TreeNode root = new TreeNode(rootVal);
if(leftNodes == 0){
root.left = null;
}else{
root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1);
}
if(rightNodes == 0){
root.right = null;
}else{
root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n);
}
return root;
}
public int findIndex(int target, int arr[], int from, int to){
for(int i = from ; i <= to ; i++){
if(arr[i] == target){
return i;
}
}
return -1;
}
}
复制代码
总结:
- 对于复杂问题,必定要划分红若干子问题,逐一求解。好比二叉树问题,咱们一般将其划分红头结点、左子树、右子树。
- 对于递归过程的参数对应关系,尽可能使用和数据样本自己没有直接关系的变量来表示。好比此题应该选取
leftNodes
和rightNodes
来计算i',j',m',n'
而不该该使用头结点在中序序列的下标index
(它和in
是绑定的,那么可能对pre
就不适用了)。
用两个栈来实现一个队列,完成队列的Push和Pop操做。 队列中的元素为int类型。
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
public void push(int node) {
}
public int pop() {
}
复制代码
这道题只要记住如下几点便可:
stack1
)只能用来存,另外一个栈(如stack2
)只能用来取stack2
是否为空,若是不空直接stack2.pop()
,不然将stack1
中的元素所有倒入stack2
,若是倒入以后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() {
if(stack2.empty()){
while(!stack1.empty()){
stack2.push(stack1.pop());
}
}
if(stack2.empty()){
throw new IllegalStateException("no more element!");
}
return stack2.pop();
}
}
复制代码
总结:只要取元素的栈不为空,取元素时直接弹出其栈顶元素便可,只有当其为空时才考虑将存元素的栈倒入进来,而且要一次性倒完。
把一个数组最开始的若干个元素搬到数组的末尾,咱们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的全部元素都大于0,若数组大小为0,请返回0。
public int minNumberInRotateArray(int [] arr) {
}
复制代码
此题需先认真审题:
由此不可贵出以下input check
:
public int minNumberInRotateArray(int [] arr) {
//input check
if(arr == null || arr.length == 0){
return 0;
}
//if only one element or no rotate
if(arr.length == 1 || arr[0] < arr[arr.length - 1]){
return arr[0];
}
//TODO
}
复制代码
上述的arr[0] < arr[arr.length - 1]
不能写成arr[0] <= arr[arr.length - 1]
,好比可能会有[1,2,3,3,4] -> [3,4,1,2,3]
的状况,这时你不能返回arr[0]=3
。
若是走到了程序中的TODO
,就能够考虑广泛状况下的推敲,数组能够被分红两部分:大于等于arr[0]
的左半部分和小于等于arr[arr.length - 1]
右半部分,咱们不妨借助两个指针从数组的头、尾向中间靠近,这样就能利用二分的思想快速移动指针从而淘汰一些不在考虑范围以内的数。
如图,咱们不能直接经过arr[mid]
和arr[l]
(或arr[r]
)的比较(arr[mid] >= arr[l]
)来决定移动l
仍是r
到mid
上,由于数组可能存在若干相同且相邻的数,所以咱们还须要加上一个限制条件:arr[l + 1] >= arr[l] && arr[mid] >= arr[l]
(对于r
来讲则是arr[r - 1] <= arr[r] && arr[mid] <= arr[r]
),即当左半部分(右半部分)不止一个数时,咱们才可能去移动l
(r
)指针。完整代码以下:
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] arr) {
//input check
if(arr == null || arr.length == 0){
return 0;
}
//if only one element or no rotate
if(arr.length == 1 || arr[0] < arr[arr.length - 1]){
return arr[0];
}
//has rotate, left part is big than right part
int l = 0, r = arr.length - 1, mid;
//l~r has more than 3 elements
while(r > l && r - l != 1){
//r-l >= 2 -> mid > l
mid = l + ((r - l) >> 1);
if(arr[l + 1] >= arr[l] && arr[mid] >= arr[l]){
l = mid;
}else{
r = mid;
}
}
return arr[r];
}
}
复制代码
总结:审题时要充分考虑数据样本的极端状况,以写出鲁棒性较强的代码。
你们都知道斐波那契数列,如今要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39
public int Fibonacci(int n) {
}
复制代码
对于公式f(n) = f(n-1) + f(n-2)
,明显就是一个递归调用,所以根据f(0) = 0
和f(1) = 1
咱们不难写出以下代码:
public int Fibonacci(int n) {
if(n == 0 || n == 1){
return n;
}
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
复制代码
在上述递归过程当中,你会发现有不少计算过程是重复的:
动态规划就在使用递归调用自上而下分析过程当中发现有不少重复计算的子过程,因而采用自下而上的方式将每一个子状态缓存下来,这样对于上层而言只有当须要的子过程结果不在缓存中时才会计算一次,所以每一个子过程都只会被计算一次。
public int Fibonacci(int n) {
if(n == 0 || n == 1){
return n;
}
//n1 -> f(n-1), n2 -> f(n-2)
int n1 = 1, n2 = 0;
//从f(2)开始算起
int N = 2, res = 0;
while(N++ <= n){
//每次计算后更新缓存,固然你也可使用一个一维数组保存每次的计算结果,只额外空间复杂度就变为O(n)了
res = n1 + n2;
n2 = n1;
n1 = res;
}
return res;
}
复制代码
上述代码不少人都能写出来,只是没有意识到这就是动态规划。
总结:当你自上而下分析递归时发现有不少子过程被重复计算,那么就应该考虑可否经过自下而上将每一个子过程的计算结果缓存下来。
一只青蛙一次能够跳上1级台阶,也能够跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(前后次序不一样算不一样的结果)。
public int JumpFloor(int target) {
}
复制代码
将复杂问题分解:复杂问题就是不断地将target
减1或减2(对应跳一级和跳两级台阶)直到target
变为1或2(对应只剩下一层或两层台阶)时咱们可以很容易地得出结果。所以对于当前的青蛙而言,它可以选择的就是跳一级或跳二级,剩下的台阶有多少种跳法交给子过程来解决:
public int JumpFloor(int target) {
//input check
if(target <= 0){
return 0;
}
//base case
if(target == 1){
return 1;
}
if(target == 2){
return 2;
}
return JumpFloor(target - 1) + JumpFloor(target - 2);
}
复制代码
你会发现这其实就是一个斐波那契数列,只不过是从f(1) = 1,f(2) = 2
开始的斐波那契数列罢了。天然你也应该可以写出动态规划版本。
一只青蛙一次能够跳上1级台阶,也能够跳上2级……它也能够跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
本质上仍是分解,只不过上一个是分解成两步,而这个是分解成n步:
public int JumpFloorII(int target) {
if(target <= 0){
return 0;
}
//base case,当target=0时表示某个分解分支跳完了全部台阶,这个分支就是一种跳法
if(target == 0){
return 1;
}
//本过程要收集的跳法的总数
int res = 0;
for(int i = 1 ; i <= target ; i++){
//本次选择,选择跳i阶台阶,剩下的台阶交给子过程,每一个选择就表明一个分解分支
res += JumpFloorII(target - i);
}
return res;
}
复制代码
这个动态规划就有一点难度了,首先咱们要肯定缓存目标,斐波那契数列中因为f(n)
只依赖于f(n-1)
和f(n-2)
所以咱们仅用两个缓存变量实现了动态规划,可是这里f(n)
依赖的是f(0),f(1),f(2),...,f(n-1)
,所以咱们须要经过长度量级为n
的表缓存前n
个状态(int arr[] = new int[target + 1]
,arr[target]
表示f(n)
)。而后根据递归版本(一般是base case
)肯定哪些状态的值是能够直接肯定的,好比由if(target == 0){ return 1 }
可知arr[0] = 1
,从f(N = 1)
开始的全部状态都须要依赖以前(f(n < N)
)的全部状态:
int res = 0;
for(int i = 1 ; i <= target ; i++){
res += JumpFloorII(target - i);
}
return res
复制代码
所以咱们能够据此自下而上计算出每一个子状态的值:
public int JumpFloorII(int target) {
if(target <= 0){
return 0;
}
int arr[] = new int[target + 1];
arr[0] = 1;
for(int i = 1 ; i < arr.length ; i++){
for(int j = 0 ; j < i ; j++){
arr[i] += arr[j];
}
}
return arr[target];
}
复制代码
但这仍不是最优解,由于观察循环体你会发现,每次f(n)
的计算都要从f(0)
累加到f(n-1)
,咱们彻底能够将这个累加值缓存起来preSum
,每计算出一次f(N)
以后都将缓存更新为preSum += f(N)
。如此获得最优解:
public int JumpFloorII(int target) {
if(target <= 0){
return 0;
}
int arr[] = new int[target + 1];
arr[0] = 1;
int preSum = arr[0];
for(int i = 1 ; i < arr.length ; i++){
arr[i] = preSum;
preSum += arr[i];
}
return arr[target];
}
复制代码
咱们能够用2*1
的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1
的小矩形无重叠地覆盖一个2*n
的大矩形,总共有多少种方法?
public int RectCover(int target) {
}
复制代码
有了以前的历练,咱们能很快的写出递归版本:先竖着放一个或者先横着放两个,剩下的交给递归处理:
//target 大矩形的边长,也是剩余小矩形的个数
public int RectCover(int target) {
if(target <= 0){
return 0;
}
if(target == 1 || target == 2){
return target;
}
return RectCover(target - 1) + RectCover(target - 2);
}
复制代码
这仍然是个以f(1)=1,f(2)=2
开头的斐波那契数列:
//target 大矩形的边长,也是剩余小矩形的个数
public int RectCover(int target) {
if(target <= 0){
return 0;
}
if(target == 1 || target == 2){
return target;
}
//n_1->f(n-1), n_2->f(n-2),从f(N=3)开始算起
int n_1 = 2, n_2 = 1, N = 3, res = 0;
while(N++ <= target){
res = n_1 + n_2;
n_2 = n_1;
n_1 = res;
}
return res;
}
复制代码
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
public int NumberOf1(int n) {
}
复制代码
题目已经给咱们下降了难度:负数用补码(取反加1)表示代表输入的参数为均为正数,咱们只需统计其二进制表示中1的个数、运算时只考虑无符号移位便可。
典型的判断某个二进制位上是否为1的方法是将该二进制数右移至该二进制位为最低位而后与1相与&
,因为1的二进制表示中只有最低位为1其他位均为0,所以相与后的结果与该二进制位上的数相同。据此不难写出以下代码:
public int NumberOf1(int n) {
int count = 0;
for(int i = 0 ; i < 32 ; i++){
count += ((n >> i) & 1);
}
return count;
}
复制代码
固然了,还有一种比较秀的解法就是利用n = n & (n - 1)
将n
的二进制位中为1的最低位置为0(只要n
不为0就说明含有二进位制为1的位,如此这样的操做能作多少次就说明有多少个二进制位为1的位):
public int NumberOf1(int n) {
int count = 0;
while(n != 0){
count++;
n &= (n - 1);
}
return count;
}
复制代码
给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
public double Power(double base, int exponent) {
}
复制代码
这是一道充满危险色彩的题,求职者可能会心里窃喜不假思索的写出以下代码:
public double Power(double base, int exponent) {
double res = 1;
for(int i = 1 ; i <= exponent ; i++){
res *= base;
}
return res;
}
复制代码
可是你有没有想过底数base
和幂exponent
都是可正、可负、可为0的。若是幂为负数,那么底数就不能为0,不然应该抛出算术异常:
//是不是负数
boolean minus = false;
//若是存在分母
if(exponent < 0){
minus = true;
exponent = -exponent;
if(base == 0){
throw new ArithmeticException("/ by zero");
}
}
复制代码
若是幂为0,那么根据任何不为0的数的0次方为1,0的0次方未定义,应该有以下判断:
//若是指数为0
if(exponent == 0){
if(base != 0){
return 1;
}else{
throw new ArithmeticException("0^0 is undefined");
}
}
复制代码
剩下的就是计算乘方结果,可是不要忘了若是幂为负须要将结果取倒数:
//指数不为0且分母也不为0,正常计算并返回整数或分数
double res = 1;
for(int i = 1 ; i <= exponent ; i++){
res *= base;
}
if(minus){
return 1/res;
}else{
return res;
}
复制代码
也许你还能够锦上添花为幂乘方的计算引入二分计算(当幂为偶数时2^n = 2^(n/2) * 2^(n/2)
):
public double binaryPower(double base, int exp){
if(exp == 1){
return base;
}
double res = 1;
res *= (binaryPower(base, exp/2) * binaryPower(base, exp/2));
return exp % 2 == 0 ? res : res * base;
}
复制代码
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得全部的奇数位于数组的前半部分,全部的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
public void reOrderArray(int [] arr) {
}
复制代码
读题以后发现这个跟快排的partition
思路很像,都是选取一个比较基准将数组分红两部分,固然你也能够以arr[i] % 2 == 0
为基准将奇数放前半部分,将偶数放有半部分,可是虽然只需O(n)
的时间复杂度但不能保证调整后奇数之间、偶数之间的相对位置:
public void reOrderArray(int [] arr) {
if(arr == null || arr.length == 0){
return;
}
int odd = -1;
for(int i = 0 ; i < arr.length ; i++){
if(arr[i] % 2 == 1){
swap(arr, ++odd, i);
}
}
}
public void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
复制代码
涉及到排序稳定性,咱们天然可以想到插入排序,从数组的第二个元素开始向后依次肯定每一个元素应处的位置,肯定的逻辑是:将该数与前一个数比较,若是比前一个数小则与前一个数交换位置并在交换位置后继续与前一个数比较直到前一个数小于等于该数或者已达数组首部中止。
此题不过是将比较的逻辑由数值的大小改成:当前的数是不是奇数而且前一个数是偶数,是则递归向前交换位置。代码示例以下:
public void reOrderArray(int [] arr) {
if(arr == null || arr.length == 0){
return;
}
int odd = -1;
for(int i = 1 ; i < arr.length ; i++){
for(int j = i ; j >= 1 ; j--){
if(arr[j] % 2 == 1 && arr[j - 1] % 2 == 0){
swap(arr, j, j - 1);
}
}
}
}
复制代码
输入一个链表,输出该链表中倒数第k个结点。
public ListNode FindKthToTail(ListNode head,int k) {
}
复制代码
倒数,这又是一个从尾到头的遍历逻辑,而链表对从尾到头遍历是敏感的,前面咱们有经过压栈/递归、反转链表的方式实现这个遍历逻辑,天然对于此题一样适用,可是那样未免太麻烦了,咱们能够经过两个间距为(k-1)个结点的链表指针来达到此目的。
public ListNode FindKthToTail(ListNode head,int k) {
//input check
if(head == null || k <= 0){
return null;
}
ListNode tmp = new ListNode(0);
tmp.next = head;
ListNode p1 = tmp, p2 = tmp;
while(k > 0 && p1.next != null){
p1 = p1.next;
k--;
}
//length < k
if(k != 0){
return null;
}
while(p1 != null){
p1 = p1.next;
p2 = p2.next;
}
tmp = null; //help gc
return p2;
}
复制代码
这里使用了一个技巧,就是建立一个临时结点tmp
做为两个指针的初始指向,以模拟p1
先走k
步以后,p2
才开始走,没走时停留在初始位置的逻辑,有利于帮咱们梳理指针在对应位置上的意义,这样当p1
走到头时(p1=null
),p2
就是倒数第k
个结点。
这里还有一个坑就是,笔者层试图为了简化代码将上述的9 ~ 12
行写成以下偷懒模式而致使排错许久:
while(k-- > 0 && p1.next != null){
p1 = p1.next;
}
复制代码
缘由是将k--
写在while()
中,不管判断是否经过都会执行k = k - 1
,所以代码老是会在if(k != 0)
处返回null
,但愿读者不要和笔者同样粗心。
总结:当遇到复杂的指针操做时,咱们不妨试图多引入几个指针或者临时结点,以方便梳理咱们的思路,增强代码的逻辑化,这些空间复杂度
O(1)
的操做一般也不会影响性能。
输入两个单调递增的链表,输出两个链表合成后的链表,固然咱们须要合成后的链表知足单调不减规则。
public ListNode Merge(ListNode list1,ListNode list2) {
}
复制代码
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1 == null || list2 == null){
return list1 == null ? list2 : list1;
}
ListNode newHead = list1.val < list2.val ? list1 : list2;
ListNode p1 = (newHead == list1) ? list1.next : list1;
ListNode p2 = (newHead == list2) ? list2.next : list2;
ListNode p = newHead;
while(p1 != null && p2 != null){
if(p1.val <= p2.val){
p.next = p1;
p1 = p1.next;
}else{
p.next = p2;
p2 = p2.next;
}
p = p.next;
}
while(p1 != null){
p.next = p1;
p = p.next;
p1 = p1.next;
}
while(p2 != null){
p.next = p2;
p = p.next;
p2 = p2.next;
}
return newHead;
}
复制代码
输入两棵二叉树A,B,判断B是否是A的子结构。(ps:咱们约定空树不是任意一个树的子结构)
/** public class TreeNode { int val = 0; TreeNode left = null; TreeNode right = null; public TreeNode(int val) { this.val = val; } }*/
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
if(root1 == null || root2 == null){
return false;
}
return process(root1, root2);
}
复制代码
这是一道典型的分解求解的复杂问题。典型的二叉树分解:遍历头结点、遍历左子树、遍历右子树。首先按照root1
和root2
的值是否相等划分为两种状况:
root2.left
也是roo1.left
的子结构(递归)、root2.right
也是root1.right
的子结构(递归),那么可返回true
。root2
为root1.left
的子结构或者root2
为root1.right
的子结构时,才能返回true
据上述两点很容易得出以下递归逻辑:
if(root1.val == root2.val){
if(process(root1.left, root2.left) && process(root1.right, root2.right)){
return true;
}
}
return process(root1.left, root2) || process(root1.right, root2);
复制代码
接下来肯定递归的终止条件,若是某个子过程root2=null
那么说明在自上而下的比较过程当中root2
的结点已被罗列比较完了,这时不管root1
是否为null
,该子过程都应该返回true
:
if(root2 == null){
return true;
}
复制代码
可是若是root2 != null
而root1 = null
,则应返回false
if(root1 == null && root2 != null){
return false;
}
复制代码
完整代码以下:
public class Solution {
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
if(root1 == null || root2 == null){
return false;
}
return process(root1, root2);
}
public boolean process(TreeNode root1, TreeNode root2){
if(root2 == null){
return true;
}
if(root1 == null && root2 != null){
return false;
}
if(root1.val == root2.val){
if(process(root1.left, root2.left) && process(root1.right, root2.right)){
return true;
}
}
return process(root1.left, root2) || process(root1.right, root2);
}
}
复制代码
操做给定的二叉树,将其变换为源二叉树的镜像。
public void Mirror(TreeNode root) {
}
复制代码
由图可知获取二叉树的镜像就是将原树的每一个结点的左右孩子交换一下位置(这个规律必定要会找),也就是说咱们只需遍历每一个结点并交换left,right
的引用指向就能够了,而咱们有成熟的先序遍历:
public void Mirror(TreeNode root) {
if(root == null){
return;
}
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
Mirror(root.left);
Mirror(root.right);
}
复制代码
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每个数字,例如,若是输入以下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
public ArrayList<Integer> printMatrix(int [][] matrix) {
}
复制代码
只要分析清楚了打印思路(左上角和右下角便可肯定一条打印轨迹)后,此题主要考查条件控制的把握。只要给我一个左上角的点(i,j)
和右下角的点(m,n)
,就能够将这一圈的打印分解为四步:
可是若是左上角和右下角的点在一行或一列上那就不必分解,直接打印改行或该列便可,打印的逻辑以下:
public void printEdge(int[][] matrix, int i, int j, int m, int n, ArrayList<Integer> res){
if(i == m && j == n){
res.add(matrix[i][j]);
return;
}
if(i == m || j == n){
//only one while will be execute
while(i < m){
res.add(matrix[i++][j]);
}
while(j < n){
res.add(matrix[i][j++]);
}
res.add(matrix[m][n]);
return;
}
int p = i, q = j;
while(q < n){
res.add(matrix[p][q++]);
}
//q == n
while(p < m){
res.add(matrix[p++][q]);
}
//p == m
while(q > j){
res.add(matrix[p][q--]);
}
//q == j
while(p > i){
res.add(matrix[p--][q]);
}
//p == i
}
复制代码
接着咱们将每一个圈的左上角和右下角传入该函数便可:
public ArrayList<Integer> printMatrix(int [][] matrix) {
ArrayList<Integer> res = new ArrayList<Integer>();
if(matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0){
return res;
}
int i = 0, j = 0, m = matrix.length - 1, n = matrix[0].length - 1;
while(i <= m && j <= n){
printEdge(matrix, i++, j++, m--, n--, res);
}
return res;
}
复制代码
定义栈的数据结构,请在该类型中实现一个可以获得栈中所含最小元素的min函数(时间复杂度应为O(1))。
public class Solution {
public void push(int node) {
}
public void pop() {
}
public int top() {
}
public int min() {
}
}
复制代码
最直接的思路是使用一个变量保存栈中现有元素的最小值,但这只对只存不取的栈有效,当弹出的值不是最小值时还没什么影响,但当弹出最小值后咱们就没法获取当前栈中的最小值。解决思路是使用一个最小值栈,栈顶老是保存当前栈中的最小值,每次数据栈存入数据时最小值栈就要相应的将存入后的最小值压入栈顶:
private Stack<Integer> dataStack = new Stack();
private Stack<Integer> minStack = new Stack();
public void push(int node) {
dataStack.push(node);
if(!minStack.empty() && minStack.peek() < node){
minStack.push(minStack.peek());
}else{
minStack.push(node);
}
}
public void pop() {
if(!dataStack.empty()){
dataStack.pop();
minStack.pop();
}
}
public int top() {
if(!dataStack.empty()){
return dataStack.peek();
}
throw new IllegalStateException("stack is empty");
}
public int min() {
if(!dataStack.empty()){
return minStack.peek();
}
throw new IllegalStateException("stack is empty");
}
复制代码
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的全部数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不多是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)
public boolean IsPopOrder(int [] arr1,int [] arr2) {
}
复制代码
可使用两个指针i,j
,初始时i
指向压入序列的第一个,j
指向弹出序列的第一个,试图将压入序列按照顺序压入栈中:
arr1[i] != arr2[j]
,那么将arr1[i]
压入栈中并后移i
(表示arr1[i]
还没到该它弹出的时刻)i
以后发现arr1[i] == arr2[j]
,那么说明此刻的arr1[i]
被压入后应该被当即弹出才会产生给定的弹出序列,因而不压入arr1[i]
(表示压入并弹出了)并后移i
,j
也要后移(表示弹出序列的arr2[j]
记录已产生,接着产生或许的弹出记录便可)。i
,所以循环的终止条件是i
到达arr1.length
,此时若栈中还有元素,那么从栈顶到栈底造成的序列必须与arr2
中j
以后的序列相同才能返回true
。public boolean IsPopOrder(int [] arr1,int [] arr2) {
//input check
if(arr1 == null || arr2 == null || arr1.length != arr2.length || arr1.length == 0){
return false;
}
Stack<Integer> stack = new Stack();
int length = arr1.length;
int i = 0, j = 0;
while(i < length && j < length){
if(arr1[i] != arr2[j]){
stack.push(arr1[i++]);
}else{
i++;
j++;
}
}
while(j < length){
if(arr2[j] != stack.peek()){
return false;
}else{
stack.pop();
j++;
}
}
return stack.empty() && j == length;
}
复制代码
从上往下打印出二叉树的每一个节点,同层节点从左至右打印。
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
}
复制代码
使用一个队列来保存当前遍历结点的孩子结点,首先将根节点加入队列中,而后进行队列非空循环:
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
ArrayList<Integer> res = new ArrayList<Integer>();
if(root == null){
return res;
}
LinkedList<TreeNode> queue = new LinkedList();
queue.addLast(root);
while(queue.size() > 0){
TreeNode node = queue.pollFirst();
res.add(node.val);
if(node.left != null){
queue.addLast(node.left);
}
if(node.right != null){
queue.addLast(node.right);
}
}
return res;
}
复制代码
输入一个整数数组,判断该数组是否是某二叉搜索树的后序遍历的结果。若是是则输出Yes,不然输出No。假设输入的数组的任意两个数字都互不相同。
public boolean VerifySquenceOfBST(int [] sequence) {
}
复制代码
对于二叉树的后序序列,咱们可以肯定最后一个数就是根结点,还能肯定的是前一半部分是左子树的后序序列,后一部分是右子树的后序序列。
遇到这种复杂问题,咱们仍能采用三步走战略(根结点、左子树、右子树):
据此咱们须要定义一个递归体,该递归体须要收集的信息以下:下层须要向我返回其最大值、最小值、以及是不是BST
class Info{
boolean isBST;
int max;
int min;
Info(boolean isBST, int max, int min){
this.isBST = isBST;
this.max = max;
this.min = min;
}
}
复制代码
递归体的定义以下:
public Info process(int[] arr, int start, int end){
if(start < 0 || end > arr.length - 1 || start > end){
throw new IllegalArgumentException("invalid input");
}
//base case : only one node
if(start == end){
return new Info(true, arr[end], arr[end]);
}
int root = arr[end];
Info left, right;
//not exist left child
if(arr[start] > root){
right = process(arr, start, end - 1);
return new Info(root < right.min && right.isBST,
Math.max(root, right.max), Math.min(root, right.min));
}
//not exist right child
if(arr[end - 1] < root){
left = process(arr, start, end - 1);
return new Info(root > left.max && left.isBST,
Math.max(root, left.max), Math.min(root, left.min));
}
int l = 0, r = end - 1;
while(r > l && r - l != 1){
int mid = l + ((r - l) >> 1);
if(arr[mid] > root){
r = mid;
}else{
l = mid;
}
}
left = process(arr, start, l);
right = process(arr, r, end - 1);
return new Info(left.isBST && right.isBST && root > left.max && root < right.min,
right.max, left.min);
}
复制代码
总结:二叉树相关的信息收集问题分步走:
- 分析当前状态须要收集的信息
- 根据下层传来的信息加工出当前状态的信息
- 肯定递归终止条件
输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的全部路径。路径定义为从树的根结点开始往下一直到叶结点所通过的结点造成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前)
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
}
复制代码
审题可知,咱们须要有一个自上而下从根结点到每一个叶子结点的遍历思路,而先序遍历恰好能够拿来用,咱们只需在来到当前结点时将当前结点值加入到栈中,在离开当前结点时再将栈中保存的当前结点的值弹出便可使用栈模拟保存自上而下通过的结点,从而实如今来到每一个叶子结点时只需判断栈中数值之和是否为target
便可。
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
ArrayList<ArrayList<Integer>> res = new ArrayList();
if(root == null){
return res;
}
Stack<Integer> stack = new Stack<Integer>();
preOrder(root, stack, 0, target, res);
return res;
}
public void preOrder(TreeNode root, Stack<Integer> stack, int sum, int target, ArrayList<ArrayList<Integer>> res){
if(root == null){
return;
}
stack.push(root.val);
sum += root.val;
//leaf node
if(root.left == null && root.right == null && sum == target){
ArrayList<Integer> one = new ArrayList();
one.addAll(stack);
res.add(one);
}
preOrder(root.left, stack, sum, target, res);
preOrder(root.right, stack, sum, target, res);
sum -= stack.pop();
}
复制代码
输入一个复杂链表(每一个节点中有节点值,以及两个指针,一个指向下一个节点,另外一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,不然判题程序会直接返回空)
/* public class RandomListNode { int label; RandomListNode next = null; RandomListNode random = null; RandomListNode(int label) { this.label = label; } } */
public class Solution {
public RandomListNode Clone(RandomListNode pHead) {
}
}
复制代码
此题主要的难点在于random
指针的处理。
能够将链表中的结点都复制一份,用一个哈希表来保存,key
是源结点,value
就是副本结点,而后遍历key
取出每一个对应的value
将副本结点的next
指针和random
指针设置好:
public RandomListNode Clone(RandomListNode pHead){
if(pHead == null){
return null;
}
HashMap<RandomListNode, RandomListNode> map = new HashMap();
RandomListNode p = pHead;
//copy
while(p != null){
RandomListNode cp = new RandomListNode(p.label);
map.put(p, cp);
p = p.next;
}
//link
p = pHead;
while(p != null){
RandomListNode cp = map.get(p);
cp.next = (p.next == null) ? null : map.get(p.next);
cp.random = (p.random == null) ? null : map.get(p.random);
p = p.next;
}
return map.get(pHead);
}
复制代码
首先将每一个结点复制一份并插入到对应结点以后,而后遍历链表将副本结点的random
指针设置好,最后将源结点和副本结点分离成两个链表
public RandomListNode Clone(RandomListNode pHead){
if(pHead == null){
return null;
}
RandomListNode p = pHead;
while(p != null){
RandomListNode cp = new RandomListNode(p.label);
cp.next = p.next;
p.next = cp;
p = p.next.next;
}
//more than two node
//link random pointer
p = pHead;
RandomListNode cp;
while(p != null){
cp = p.next;
cp.random = (p.random == null) ? null : p.random.next;
p = p.next.next;
}
//split source and copy
p = pHead;
RandomListNode newHead = p.next;
//p != null -> p.next != null
while(p != null){
cp = p.next;
p.next = p.next.next;
p = p.next;
cp.next = (p == null) ? null : p.next;
}
return newHead;
}
复制代码