剑指Offer第二版Java代码实现
由于本人的经验和水平有限,如有错误的地方还请不吝赐教,不胜感激!
A.单例模式
面试题 2:实现Singleton模式
public class Single1 {//单例模式,饿汉形式,不管是否为空,先new出来一个对象
//1私有构造器,让其在外部不能够顺利访问
private Single1(){
}
//2声明个静态自己本身的对象,
private static Single1 single1=new Single1();
//3声明个静态方法,以方便外部调用
public static Single1 getInstance(){
return single1;
}
}
public class Single2 {//单子模式的懒汉模式,判断对象是否为空,为空的时候才进行实例化
private Single2(){
}
private static Single2 single2;
public static Single2 getInstance(){
if (single2==null){
single2=new Single2();
}
return single2;
}
}
public class Single3 {
private Single3(){
}
private static Single3 single3;
public static Single3 getInstance(){
synchronized (Single3.class){//同步锁的加入
if (single3==null){
single3=new Single3();
}
}
return single3;
}
}
public class Single4 {
private Single4(){
}
private static Single4 single4;
public static Single4 getInstance(){
if (single4==null){
synchronized (Single4.class){
if (single4==null){
single4=new Single4();
}
}
}
return single4;
}
}
public class Single5 {
private Single5(){
}
private static Single5 single5;
static {
single5=new Single5();
}
public static Single5 getInstance(){
return single5;
}
}
public class Single6 {
private Single6(){
}
private static class Nested{
private static Single6 single6;
static {
single6=new Single6();
}
}
public static Single6 getInstance(){
return Nested.single6;
}
}
B.面试需要的基础知识
面试题 3:数组中重复的数字
/**
* 题目 3:在一个长度为n的数组里的所有数字都在0到n-1的范围内。
* 数组中某些数字是重复的,但不知道有几个数字重复,也不知道每个数字重复几次。
* 请找出数组中任意重复的数字。
*/
public class B3 {
//方法一:排序后相邻元素两两比较
public static void duplicate(int numbers[],int length,int duplication []){
if (numbers==null||length==0){
return;
}
Arrays.sort(numbers);
int j=0;
for (int i=0;i<length-1;i++){
if (numbers[i]==numbers[i+1]){
duplication[j]=numbers[i];
j++;
}
}
}
//方法二:扫描到下标为i的数字m时,比较这个数是否等于i
// 是: 扫描下一个数
// 否: 将他与第m个数字比较:
// 相等:找到一个重复数字
// 不相等:把第i个数和第m个数互换位置(把m放到属于他的位置)
public static void duplicate1(int numbers[],int length,int duplication []){
if (numbers==null||length==0){
return;
}
for(int i=0;i<length;i++){//给方法只适用于数组中所有数字在0-(n-1)范围内
if(numbers[i]<0||numbers[i]>length-1){
return;
}
}
int k=0;
for (int i=0;i<length;i++){
while(numbers[i]!=i){//下标与数字不等
if (numbers[i]==numbers[numbers[i]]){//将该数作为下标 与此下表所对应的数比较
duplication[k]=numbers[i];
k++;
break;
}
swap(numbers,i,numbers[i]);//将数与下标匹配
}
}
}
private static void swap(int[] numbers, int p, int q) {
int temp=numbers[p];
numbers[p]=numbers[q];
numbers[q]=temp;
}
//方法三:如要求不修改数组找出重复的元素,尝试避免使用O(n)的辅助空间
// 将1~n的数字从中间的数字m分为两部分:1~m和m~n
// 如1~m出现的次数大于m,那么这一半区间里一定包含重复数字(类似二分查找)
public static void duplicate2(int numbers[],int length,int duplication []){
if (numbers==null||length==0){
return;
}
int start=1;
int end=length-1;
while(end>=start){
int middle=(start+end)>>1;
int count=countRange(numbers,length,start,middle);
if (end==start){
if (count>1){
duplication[0]=start;
}
break;
}
if (count>(middle-start+1)){//带有重复元素。缩小范围
end=middle;
}else {//没有重复元素,改变start范围,继续下一个区间查找
start=middle+1;
}
}
}
private static int countRange(int[] numbers, int length, int start, int end) {
if (numbers==null){
return 0;
}
int count=0;
for (int i=0;i<length;i++){
if (numbers[i]>=start&&numbers[i]<=end){//有效元素
count++;
}
}
return count;
}
}
面试题 4:二维数组的查找
/**
* 题目 4:在一个二维数组中,每一行都按照从左到右递增的顺序排列,每一列都按照从上到下递增的顺序。
* 请完成一个函数,输入这样一个二维数组和一个整数,判断数组中是否含有该整数。
*
* 思路:首先选取数组中右上角的数字
* 如果该数字等于要查找的数字,则查找过程结束;
* 如果该数字大于要查找的数字,则剔除这个数字所在的列;
* 如果该数字小于要查找的数字,则剔除这个数字所在的行。
*/
public class B4 {
public static boolean find(int [][]matrix,int number){
if (matrix==null||matrix.length<1||matrix[0].length<1){
return false;
}
int rows=matrix.length;//行数
int cols=matrix[0].length;//列数
int col=cols-1;//选定右上角
int row=0;
while(row>=0&&row<rows&&col>=0&&col<cols){
if (matrix[row][col]==number){
return true;
}else if (matrix[row][col]>number){
col--;
}else {
row++;
}
}
return false;
}
}
面试题 5:替换空格
/**
* 题目 5 :请实现一个函数把字符串中的每个空格替换成“%20”
*/
public class B5 {
public String replaceSpace(StringBuffer str) {
int spacenum = 0;//spacenum为计算空格数
for(int i=0;i<str.length();i++){
if(str.charAt(i)==' ')
spacenum++;
}
int indexold = str.length()-1; //indexold为为替换前的str下标
int newlength = str.length() + spacenum*2;//计算空格转换成%20之后的str长度
int indexnew = newlength-1;//indexold为为把空格替换为%20后的str下标
str.setLength(newlength);//使str的长度扩大到转换成%20之后的长度,防止下标越界
for(;indexold>=0 && indexold<newlength;--indexold){
if(str.charAt(indexold) == ' '){
str.setCharAt(indexnew--, '0');
str.setCharAt(indexnew--, '2');
str.setCharAt(indexnew--, '%');
}else{
str.setCharAt(indexnew--, str.charAt(indexold));
}
}
return str.toString();
}
}
面试题 6:从尾到头打印链表
import java.util.Stack;
/**
* 题目 6:从尾到头打印链表,不允许改变链表结构
* 思路:典型的后进先出,可以用栈来实现
*/
public class B6 {
public static class ListNode{
int val;
ListNode next;
}
//方法1:循环
public static void printListInverseIteration(ListNode root){
Stack<ListNode> stack=new Stack<>();
while (root!=null){
stack.push(root);
root=root.next;
}
//遍历完链表后,遍历该栈
ListNode temp;
while (!stack.isEmpty()){
temp=stack.pop();
System.out.println(temp.val+" ");
}
}
//方法2:递归
public static void printListInverseRecursion(ListNode root){
if (root!=null){
printListInverseRecursion(root.next);
System.out.println(root.val+" ");
}
}
}
面试题 7:重建二叉树
/**
* 题目 7:重建二叉树
* 输入某二叉树的前序和中序遍历结果,请重建该二叉树
*
*/
public class B7 {
public static class BinaryTreeNode{
int value;
BinaryTreeNode left;
BinaryTreeNode right;
}
/**
*
* @param preorder 前序遍历数组
* @param inorder 中序遍历数组
* @return 根节点
*/
public static BinaryTreeNode constructor(int[] preorder,int[] inorder){
if(preorder==null||inorder==null||preorder.length!=inorder.length||inorder.length<1){
return null;
}
return constructor(preorder,0,preorder.length-1,inorder,0,inorder.length-1);
}
/**
*
* @param preorder 前序数组
* @param ps 前序数组索引开始
* @param pe 前序数组索引结束
* @param inorder 中序数组
* @param is 中序数组索引开始
* @param ie 中序数组索引结束
* @return
*/
private static BinaryTreeNode constructor(int[] preorder, int ps, int pe, int[] inorder, int is, int ie) {
if(ps>pe){
return null;
}
//取前序数组的第一个索引的值,也就是根
int value=preorder[ps];//根值
//在中序数组里判断该根所在的索引号,
int index=is;
while(index<=ie&&inorder[index]!=value){
index++;
}
//已经在中序数组内找到了该根,知道了其索引号
if(index>ie){
throw new RuntimeException("错误的输入");
}
BinaryTreeNode node=new BinaryTreeNode();
node.value=value;
node.left=constructor(preorder,ps+1,ps+index-is,inorder,is,index-1);
node.right=constructor(preorder,ps+index-is+1,pe,inorder,index+1,ie);
return node;
}
}
面试题 8: 二叉树的下一个节点
/**
* 题目 8:二叉树的下一个节点
* 给定一棵二叉树和其中的一个节点,如何找出他的下一个节点?
* 树中的节点除了有两个分别指向左,右节点的指针,还有一个指向父节点的指针。
*
* 思路:
* 根据中序遍历,找到某个节点的下一个节点,中序遍历(左根右)
*/
public class B8 {
private class TreeLinkNode{
int val;
TreeLinkNode left=null;
TreeLinkNode right=null;
TreeLinkNode parent=null;
TreeLinkNode(int vaa){
this.val=vaa;
}
TreeLinkNode(){}
}
private TreeLinkNode getNext(TreeLinkNode pNode){
//如果当前节点右子树不为空,那么中序遍历的下一个节点就是其右子树的最左子节点(如存在)
if (pNode.right!=null){
pNode=pNode.right;
while(pNode.left!=null){
pNode=pNode.left;
}
return pNode;
}
//如果当前子节点的右子树为空,返回上层节点,
// 如果父节点的右子节点就是当前节点,继续返回上层的父节点
//直到父节点的左子节点等于当前节点
while(pNode.parent.right!=null&&pNode.parent.right==pNode){
pNode=pNode.parent;
if ( pNode.parent==null){
return null;
}
}
return pNode.parent;
}
}
面试题 9:用两个栈实现队列
import java.util.Stack;
/**
* 题目 9:用两个栈实现队列
*/
public class B9 {
public static class MyQueue{
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
public void add(int node) {
stack1.add(node);
}
public int remove() {
if(stack2.isEmpty()){
while(!stack1.isEmpty()){
stack2.push(stack1.pop());
}
}
if(stack2.isEmpty()){
throw new RuntimeException("没有更多元素了");
}
return stack2.pop();
}
}
}
面试题 10:斐波那契数列
面试题 10(1):求斐波那契数列的第n项
/**
* 题目10(1):求斐波那契数列的第n项
* 1、1、2、3、5、8、13、21、34
*
*/
public class B10 {
public long fibonacci(int n){//递归实现
if(n<=0){
return 0;
}
if (n==1){
return 1;
}
return fibonacci(n-1)+fibonacci(n-2);
}
public long fibonacci1(int n){//循环实现
if (n<=0){
return 0;
}
if (n==1||n==2){
return 1;
}
int prePre=1;
int pre=1;
int curent=2;
for (int i=3;i<=n;i++){
curent=prePre+pre;
prePre=pre;
pre=curent;
}
return curent;
}
}
面试题 10 (2):青蛙跳台阶
/**
* 题目:青蛙跳台阶
* 一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 阶台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法
* 1、2、3、5、8、13、21、34
*/
public class B101 {
public long jump(int n){
if(n<=0){
return 0;
}
if (n==1){
return 1;
}
if (n==2){
return 2;
}
return jump(n-1)+jump(n-2);
}
}
面试题 11:旋转数组中最小数字
/**
* 面试题11:旋转数组中的最小数字
* 把一个数组最开始的若干个元素搬到数组的尾部,我们称之为一个数组的旋转。
* 输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素
* 例如数组{3, 4, 5, 1,2}为数组{1 ,2 ,3, 4, 5}的一个旋转,该数组的最小元素为 1
*/
public class B11 {
public int myMin(Integer[] numbers) {
//判断是否输入合法
if (numbers==null||numbers.length==0){
throw new RuntimeException("错误的输入");
}
int lo=0;
int hi=numbers.length-1;
int m=0;
while(numbers[lo]>=numbers[hi]){
if (hi-lo==1){//退出循环条件
return numbers[hi];
}
m=(lo+hi)>>1;
//1, 0, 1, 1, 1不适于该方法
if (numbers[lo]==numbers[m]&&numbers[hi]==numbers[m]){
return minOrder(numbers,lo,hi);
}
//3, 4, 5, 1, 2
//判断m在哪段中,并缩小范围
if (numbers[m]>=numbers[lo]){
lo=m;
}else if (numbers[m]<=numbers[hi]){
hi=m;
}
}
return numbers[0];
}
private int minOrder(Integer[] numbers,int start,int end) {
int min=numbers[start];
for (int i=start+1;i<=end;i++){
if (numbers[i]<min){
min=numbers[i];
}
}
return min;
}
}
面试题 11 扩展1 :快速排序
/**
* 题目:快排
*/
public class B111 {
public void quicksort(int []a,int start,int end){
if (a==null){
return;
}
if (start<0||start>a.length-1||end<0||end>a.length-1){
return;
}
int h=start;
int e=end;
if (h>=e){
return;
}
boolean flag=true;//true向左,false向右
while (h<e){
if (a[h]>a[e]){
int temp=a[h];
a[h]=a[e];
a[e]=temp;
flag=!flag;
}
if (flag){
e--;
}else{
h++;
}
}
h++;
e--;
quicksort(a,start,e);
quicksort(a,h,end);
}
}
面试题 12:矩阵中的路径
/**
* 题目12:矩阵中的路径
* 请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。
* 路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子【相邻的格子,不能跨格子】。
* 如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。
*/
public class B12 {
private static boolean hasPath(char[][] matrix, char[] str) {
if (matrix==null||str==null){
return false;
}
int rows=matrix.length;
int cols=matrix[0].length;
//布尔矩阵 对应matrix的每个元素,初始值是false,代表该元素没有被访问过
boolean[][] visited=new boolean[rows][cols];
int pathLength=0;
for (int i=0;i<rows;i++){
for (int j=0;j<cols;j++){
if (hashPathCore(matrix,str,visited,i,j,pathLength)){
return true;
}
}
}
return false;
}
/**
*
* @param matrix 比对的矩阵
* @param str 查找的字符数组
* @param visited 用于判断是否访问过的布尔类型矩阵
* @param r 行索引
* @param c 列索引
* @param pathLength 计数器
* @return true:找到了元素
* false:没有找到元素
*/
private static boolean hashPathCore(char[][] matrix, char[] str, boolean[][] visited, int r, int c, int pathLength) {
int m=matrix.length;
int n=matrix[0].length;
if (pathLength==str.length){//该计数器是否等于要查找的字符数组
return true;//相等代表当前行已经找到该元素,否则需继续查找
}
boolean hasPath=false;
if (r>=0&&r<m&&c>=0&c<n&&matrix[r][c]==str[pathLength]&&visited[r][c]==false){
//当行列索引在合法范围内,且没有被访问过时,如元素符合则
visited[r][c]=true;//暂定
pathLength++;//暂定
hasPath=hashPathCore(matrix, str, visited, r, c-1, pathLength)
||hashPathCore(matrix, str, visited, r, c+1, pathLength)
||hashPathCore(matrix, str, visited, r-1, c, pathLength)
||hashPathCore(matrix, str, visited, r+1, c, pathLength);
if (!hasPath){//不符合则回滚
pathLength--;
visited[r][c]=false;
}
}
return hasPath;
}
}
面试题 13 :机械人的运动范围
/**
* 题目13:机械人的运动范围
* 地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,
* 每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。
* 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),
* 因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
*/
public class B13 {
/**
*
* @param threshold 门限值,题目中的k
* @param rows 行数
* @param cols 列数
* @return 能到达的格数
*/
public static int movingCount(int threshold,int rows,int cols){
if (rows<=0||cols<=0||threshold<0){
return 0;
}
boolean marked[]=new boolean[rows*cols];
return move(0,0,threshold,rows,cols,marked);
}
/**
*
* @param r 行索引
* @param c 列索引
* @param threshold 门限值
* @param rows 行数
* @param cols 列数
* @param marked 标记矩阵
* @return 能到达格数
*/
private static int move(int r, int c, int threshold, int rows, int cols, boolean[] marked) {
int count=0;
if (r>=0&&r<rows&&c>=0&&c<cols&&!marked[r*cols+c]&digitSum(r)+digitSum(c)<=threshold){
marked[r*cols+c]=true;
count=1+move(r, c-1, threshold, rows, cols, marked)
+move(r, c+1, threshold, rows, cols, marked)
+move(r-1, c, threshold, rows, cols, marked)
+move(r+1, c, threshold, rows, cols, marked);
}
return count;
}
/**
*
* @param number 要拆解的索引坐标
* @return 该数字的数位之和
*/
private static int digitSum(int number) {
int sum=0;
while(number>0){
sum+=number%10;
number/=10;
}
return sum;
}
}
面试题 14 :剪绳子
/**
* 题目14:剪绳子
* 给你一根长度为n绳子,请把绳子剪成m段(m、n都是整数,n>1并且m≥1)。
* 每段的绳子的长度记为k[0]、k[1]、……、k[m]。k[0]*k[1]*…*k[m]可能的最大乘
* 积是多少?例如当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此
* 时得到最大的乘积18。
*/
public class B14 {
/**
*
* @param length 绳子长度
* @return 剪完后每段绳子的最大乘积
*/
public static int maxCutting(int length){//动态规划
if (length<=1){
return 0;
}
if (length==2){
return 1;
}
if (length==3){
return 2;
}
int []pro=new int[length+1];
pro[0]=0;
pro[1]=1;
pro[2]=2;
pro[3]=3;//3>1*2,故列出
for (int i=4;i<=length;i++){
int max=0;
for (int j=1;j<=i/2;j++){//i/2避免发生重复的相乘情况
if (max<pro[j]*pro[i-j]){
max=pro[j]*pro[i-j];
}
}
pro[i]=max;
}
return pro[length];
}
public static int maxCutting1(int length){//贪婪算法
if (length<=1){
return 0;
}
if (length==2){
return 1;
}
if (length==3){
return 2;
}
//n>=5时尽可能多剪长度为3的绳子
int timesOf3=length/3;
if (length-3*timesOf3==1){
timesOf3--;//退一个,剩下长度为4:(2*2)比剩下长度为1:(1*3)更优
}
int timesOf2=(length-timesOf3*3)/2;
return (int)Math.pow(3,timesOf3)*(int)Math.pow(2,timesOf2);
}
}
面试题 15 :二进制中 1 的个数
/**
* 题目15:二进制中1的个数
* 请实现一个函数,输入一个整数,输出该数的二进制表示中1的个数。
* 如:把9表示成二进制是 1001,有 2 位是 1 。因此,如果输入 9 ,则该函数输出 2
*
* 举一反三:
* 把整数减去 1 之再和原来的整数做位与运算,得到的结果相当于
* 把整数的二进制表示中最右边的 1 变成 0
*/
public class B15 {
//方法1,2 多少位整数就循环多少次
public int numberOf1_1(int n){//左移flag
int count=0;
int flag=1;
while(flag!=0){
if ((n&flag)!=0){
count++;
}
flag=flag<<1;
}
return count;
}
public int numberOf1_2(int n){//右移整数
int count=0;
while(n!=0){
if ((n&1)==1){
count++;
}
n=n>>>1;
}
return count;
}
//整数中有多少个1 就循环多少次
public int numberOf1_3(int n){//推荐的解法
int count=0;
while (n!=0){
n=(n-1)&n;
count++;
}
return count;
}
}
C.高质量的代码
面试题 16:数值的整数次方
/**
* 题目 16:数值的整数次方
* 实现函数double Power(double base, int exponent),求 base的 exponent次方
* 不得使用库函数,同时不需要考虑大数问题。
*/
public class C16 {
public double power(double base,int exponent){
double result;
if (exponent>0){
result=powerCore(base, exponent);
}else if (exponent<0){
if (base==0){
return 0;
}
result=1/powerCore(base, -exponent);
}else {
return 1;
}
return result;
}
private double powerCore(double base, int exponent) {
if (exponent==1){
return base;
}
if (exponent==0){
return 1;
}
// a(n)=a(n/2)*a(n/2) n为偶数
//a(n)=a(n-1)/2*a(n-1)/2*a n为奇数
double result=powerCore(base, exponent>>1);
result*=result;
if ((exponent&1)==1){
result*=base;
}
return result;
}
}
面试题 17:打印从1到最大的n位数
public class C17 {
/**
*
* @param n 要打印的该数字一共有多少位
*/
public static void printToMax(int n){
if (n<=0){
return;
}
char [] digit=new char[n];
for (int i=0;i<n;i++){
digit[i]='0';
}
for (int i=0;i<n;i++){
while (digit[i]!='9'){
int m=n-1;
digit[m]++;
while (m>=0 && digit[m]>'9'){//进位
digit[m]='0';
digit[--m]++;
}
printdigits(digit);
}
}
}
private static void printdigits(char[] digit) {
int k=0;
while(digit[k]=='0'){
k++;
}
for(;k<digit.length;k++){
System.out.print(digit[k]);
}
System.out.println();
}
/**
* 当 n很大的时候,会溢出
*/
// public void print(int n){
// if(n< 0){
// return;
// }
// int num=1;
// int i=0;
// while (i++<n){
// num*=10;
// }
// for (i=1;i<num;i++){
// System.out.println(i);
// }
// }
}
面试题 18: 删除链表的节点
面试题 18(1): 在O(1)时间删除链表的节点
/**
* 题目 18(1): 在O(1)时间内删除链表节点
* 给定单向链表的头指针和一个结点指针,定义一个函数在O(1)时间内的删除给节点
*
* 注:本解法存在缺陷,要求O(1)时间,则无法确定待删除结点的确在表中
*/
public class C18 {
public class ListNode{
int val;
ListNode next;
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
public ListNode deleteNode(ListNode head,ListNode deleteNode){
if (head==null||deleteNode==null){
return head;
}
//待删除的节点不是尾结点
if(deleteNode.next!=null){//一般情况
ListNode nextNode=deleteNode.next;
deleteNode.val=nextNode.val;
deleteNode.next=nextNode.next;
nextNode=null;
}else if (head==deleteNode){//链表中只有一个节点
deleteNode=null;
head=null;
}else {//链表中有多个节点,需要删除的是尾节点
ListNode preNode=head;
while(preNode.next!=deleteNode&&preNode!=null){
preNode=preNode.next;
}
if (preNode==null){
System.out.println("需删除节点不在链表内");
return head;
}
preNode.next=null;
deleteNode=null;
}
return head;
}
}
面试题 18(2): 删除链表中重复的节点
/**
* 题目 18(2): 删除链表中重复的节点
* 如:1 2 3 3 4 4 5
* 删除重复节点后: 1 2 5
*/
public class C182 {
class ListNode{
int val;
ListNode next=null;
ListNode(int val,ListNode next){
this.val=val;
this.next=next;
}
}
public ListNode deleteDuplication(ListNode pHead){
if (pHead==null||pHead.next==null){
return pHead;
}
ListNode preNode=null;
ListNode curNode=pHead;
while (curNode!=null){
boolean needDelete=false;
if (curNode.next!=null && curNode.val==curNode.next.val){
needDelete=true;
}
if (!needDelete){
preNode=curNode;
curNode=curNode.next;
}else {//有两个连续的节点相同 进行删除
//depValue重复节点的值
int dupValue=curNode.val;
//toBeDel要删除的节点
ListNode toBeDel=curNode;
while (toBeDel!=null && toBeDel.val==dupValue){
toBeDel=toBeDel.next;
}
if (preNode==null){//说明头节点就是相同的,要删除头节点
pHead=toBeDel;
}else {//把相同节点都跃过去,和不同的节点进行关联
preNode.next=toBeDel;
}
curNode=toBeDel;
}
}
return pHead;
}
}
面试题 19:正则表达式匹配
/**
* 题目 19:正则表达式匹配
* 请实现一个函数用来匹配包含'.'和'*'的正则表达式。模式中的字符'.'
* 表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次)。在本题
* 中,匹配是指字符串的所有字符匹配整个模式。
*
* 例如,字符串 "aaa" 与模式 "a.a" 和 "ab*ac*a" 匹配,但与 "aa.a" 及 "ab*a" 均不匹配。
*/
public class C19 {
public boolean match(char [] str,char [] pattern){
if(str==null && pattern==null){
return false;
}
return matchCore(str,0,pattern,0);
}
/**
*
* @param str 字符串
* @param indexOfStr 字符串索引
* @param pattern 模式串
* @param indexOfPattern 模式串索引
* @return
*/
private boolean matchCore(char[] str, int indexOfStr, char[] pattern, int indexOfPattern) {
if (indexOfStr==str.length && indexOfPattern==pattern.length){
//全部匹配完毕
return true;
}
if (indexOfStr<str.length && indexOfPattern==pattern.length){
//字符串str没有到末尾,而pattern到达末尾
return false;
}
//如果在模式串中的第二个字符找到了 '*'
if (indexOfPattern+1<pattern.length && pattern[indexOfPattern+1]=='*'){
if (indexOfStr<str.length &&
(pattern[indexOfPattern]=='.' || pattern[indexOfPattern]==str[indexOfStr])){
//*字节匹配0次,字符串位置不变 模式串后移两个字符
return matchCore(str,indexOfStr,pattern,indexOfPattern+2)||
//*字符串匹配多次 字符串后移一个字符,模式串字符位置不变,继续比较
matchCore(str,indexOfStr+1,pattern,indexOfPattern);
}else {
return matchCore(str,indexOfStr,pattern,indexOfPattern+2);
}
}
//如果在模式串中的第一个字符找到了 '.' ,或与字符串第一个字符相等
if (indexOfStr<str.length && ( pattern[indexOfPattern]=='.' || pattern[indexOfPattern]==str[indexOfStr])){
return matchCore(str,indexOfStr+1,pattern,indexOfPattern+1);
}
return false;
}
}
面试题 20:表示数值的字符串
/**
* 题目 20:表示数值的字符串
* 请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,
* 字符串“+100”、“5e2”、“-123”、“3.1416”及“-1E-16”都表示数值,但“12e”、
* 1a3.14”、“1.2.3”、“+-5”及“12e+5.4”都不是
*
* 思路:
* 数字的格式可以用A[.[B]][e|EC]或者.B[e|EC]表示,其中A和C都是
* 整数(可以有正负号,也可以没有),而B是一个无符号整数
* 其中A为数值的整数部分,B紧跟着小数点为数值的小数部分,C紧跟着'e'或者'E'为数值的指数部分
*
*/
public class C20 {
//方法 1 正则
public boolean isNumeric(char[] str) {
String s=String.valueOf(str);
return s.matches("[+-]?[0-9]*(\\.[0-9]*)?([eE][+-]?[0-9]+)?");
}
//方法 2
/**
*
* @param str 要判断的一个字符数组 由字符串toChar转换来的
* @return true:该字符串是一个合法的整数 不是
*/
public boolean isNumber(char[] str){ //2.1E-3
if(str==null||str.length==0){
return false;
}
int [] index=new int[1];
index[0]=0;//用于记录当前字符位置
//先来判断A
boolean isNumberic;//用于记录三个部分的判断是否满足
isNumberic=isInteger(str,index);
//再判断B
if(index[0]<str.length && (str[index[0]]=='.')){
index[0]++;//
//判断B部分是否有数值部分
isNumberic=isUnsignedInteger(str,index)||isNumberic;// A 和B部分都正确,isNumber才正确
}
//判断指数部分的C
if(index[0]<str.length&&(str[index[0]]=='e'||str[index[0]]=='E')){
index[0]++;
isNumberic=isInteger(str,index)&&isNumberic;
}
if(isNumberic&&index[0]==str.length){//全部都已经判断了
return true;
}else{
return false;
}
}
/**
*
* @param str 要判断的一个字符数组 由字符串toChar转换来的
* @param index ;//用于记录当前字符位置
* @return true:该部分是否有符号+ -,判断正确 false:该部分判断失败
*/
private boolean isInteger(char[] str, int[] index) {
if(index[0]<str.length&&(str[index[0]]=='+'||str[index[0]]=='-')){//首位是否有正负号
index[0]++;
}
return isUnsignedInteger(str,index);
}
/**
*
* @param str 要判断的一个字符数组 由字符串toChar转换来的
* @param index //用于记录当前字符位置
* @return true:该部分是否在0-9范围之内,判断正确 false:该部分判断失败
*/
private boolean isUnsignedInteger(char[] str, int[] index) {
int start=index[0];
while(index[0]<str.length&&(str[index[0]]-'0'<=9 && str[index[0]]-'0'>=0)){
index[0]++;
}
if(index[0]>start){
return true;
}else{
return false;
}
}
}
面试题 21:调整数组顺序使奇数位于偶数前面
/**
* 题目 21 :调整数组顺序使奇数位于偶数前面
* 输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有
* 奇数位于数组的前半部分,所有偶数位于数组的后半部分。
*/
public class C21 {
/**
*
* @param array 要查找的数组
*/
public void reOrderArray(int [] array) {//双指针
if(array==null || array.length==0){
return;
}
int begin=0;
int end=array.length-1;
while(begin<end){
while(begin<end && (array[begin]&1)==1){
begin++;
}
while(begin<end && (array[end]&1)==0){
end--;
}
if(begin<end){
int temp=array[begin];
array[begin]=array[end];
array[end]=temp;
}
}
}
}
面试题 22:链表中倒数第k个节点
/**
* 题目 22:链表中倒数第k个节点
* 输入一个链表,输出该链表中倒数第k个结点。为了符合大多数人的习惯,
* 本题从1开始计数,即链表的尾结点是倒数第1个结点。例如一个链表有6个结点,
* 从头结点开始它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个结点是
* 值为4的结点。
*/
public class C22 {
public static class ListNode{
int val;
ListNode next=null;
ListNode(int val){
this.val=val;
}
}
public ListNode findKthToTail(ListNode head,int k ){
if (head==null ||k<=0){
return null;
}
int count=0;
ListNode node=head;
Stack<ListNode> stack=new Stack<>();
while(node!=null){
count++;
stack.push(node);
node=node.next;
}
if (k>count){
return null;
}else {
for (int i=1;i<=k;i++){
node=stack.pop();
}
return node;
}
}
public ListNode findKthToTail2(ListNode head,int k){
if (head==null||k<=0){
return null;
}
ListNode pAHead=head;
for (int i=1;i<k;i++){
pAHead=pAHead.next;
if (pAHead==null){
return null;
}
}
ListNode pBhead=head;
while(pAHead.next!=null){
pAHead=pAHead.next;
pBhead=pBhead.next;
}
return pBhead;
}
}
面试题 23:链表中环的入口节点
/**
* 题目 23: 链表中环的入口节点
* 一个链表中包含环,如何找出环的入口结点?
*
* 思路:1.确定链表是否有环:通过两个不同速度的指针确定
* 2.确定环中结点的数目n:指针走一圈,边走边计数
* 3.找到环的入口:从头结点开始,通过两个相差为n的指针来得到(即寻找链表中倒数第n个结点)
*/
public class C23 {
public static class ListNode{
int val;
ListNode next=null;
ListNode(int val){
this.val=val;
}
}
//找出当前单向链表是否有环 采用的快慢指针的方式 返回的是两个指针的相遇节点
public ListNode meetingNode(ListNode head){
if(head==null){
return null;
}
ListNode pSlow=head;
ListNode pFast=head;
while(pFast!=null){
pSlow=pSlow.next;
pFast=pFast.next;
if (pFast!=null){
pFast=pFast.next;
}
if (pSlow!=null && pFast==pSlow){
return pSlow;
}
}
return null;
}
public ListNode entryNodeOfLoop(ListNode head){
ListNode meettingNode=this.meetingNode(head);
if (meettingNode==null){
return null;
}
int count=1;
ListNode pNode1=meettingNode.next;
while(pNode1!=meettingNode){
count++;
pNode1=pNode1.next;
}
pNode1=head;
for (int i=0;i<count;i++){
pNode1=pNode1.next;
}
ListNode pNode2=head;
while(pNode1!=pNode2){
pNode1=pNode1.next;
pNode2=pNode2.next;
}
return pNode1;
}
}
面试题 24:反转链表
/**
* 题目 24:反转链表
* 定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。
*/
public class C24 {
private class ListNode{
int val;
ListNode next=null;
ListNode(int val){
this.val=val;
}
}
//方法 1:三个指针实现单链表的反转
public ListNode reverseList1(ListNode pHead){
if (pHead==null || pHead.next==null){
return pHead;
}
//之前 pPrev->pNode->pNext
//之后 pNext->pNode->pPrev
//之后 pPrev<-pNode<-pNext
ListNode pReverseHead=null;//反转之后的头节点,也就是返回节点
ListNode pNode=pHead;//当前节点
ListNode pPrev=null;//定义存储的前一个节点
while(pNode!=null){
ListNode pNext=pNode.next;
if (pNext!=null){
pReverseHead=pNode;
}
pNode.next=pPrev;
pPrev=pNode;
pNode=pNext;
}
return pReverseHead;
}
//递归反转,利用递归特性 后进先出的原则进行
public ListNode reverseList2(ListNode pHead){
if (pHead==null || pHead.next==null){
return pHead;
}
ListNode pNext=pHead.next;
ListNode reverserHead=reverseList2(pNext);
pHead.next=null;
pNext.next=pHead;
return reverserHead;
}
}
面试题 25:合并两个排序的链表
/**
* 题目 25:合并两个排序的链表
* 输入两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按照递增排序的。
*
* 递归实现:
* 合并过程中,每次都是从两个链表中找出较小的一个来链接,因此可以采用递归来实现:
* 任意一个链表为null时,直接链接另一个链表即可;其余情况只需要在两个链表中找出较小的一个结点进行链接,
* 该结点的next值继续通过递归函数来链接。
*
* 非递归实现:
* 非递归实现比较容易想到,直接进行分情况讨论即可,要稍微注意下后面代码中头结点的赋值过程。
*/
public class C25 {
private class ListNode{
int val;
ListNode next=null;
public ListNode(int val){
this.val=val;
}
public ListNode(){}
}
//递归版本
public ListNode merge(ListNode list1,ListNode list2){
if (list1==null){
return list2;
}
if (list2==null){
return list1;
}
if (list1.val<list2.val){
list1.next=merge(list1.next, list2);
return list1;
}else {
list2.next=merge(list1, list2.next);
return list2;
}
}
//循环版本
public ListNode merge2(ListNode list1,ListNode list2){
if (list1==null){
return list2;
}
if (list2==null){
return list1;
}
ListNode dumyHead=new ListNode(0);
ListNode p=dumyHead;
while(list1!=null && list2!=null){
if (list1.val<list2.val) {
p.next = list1;
list1 = list1.next;
}else {
p.next=list2;
list2=list2.next;
}
p=p.next;
}
if (list1==null){
p.next=list2;
}else {
p.next=list1;
}
return dumyHead.next;
}
}
面试题 26:树的子结构
/**
* 题目 26: 树的子结构
* 输入两棵二叉树A和B,判断B是不是A的子结构。
*
* 思路
* 1)先对A树进行遍历,找到与B树的根结点值相同的结点R;
* 2)判断A树中以R为根结点的子树是否包含B树一样的结构。
*/
public class C26 {
private class TreeNode{
double val;
TreeNode left=null;
TreeNode right=null;
public TreeNode(){
}
public TreeNode(int val){
this.val=val;
}
}
public boolean hasSubTree(TreeNode root1,TreeNode root2){
boolean result=false;
if (root1!=null && root2!=null){
if (root1.val==root2.val){
result=doesTree1HaveTree(root1, root2);
}
if (!result){
result=doesTree1HaveTree(root1.left, root2);
}
if (!result){
result=doesTree1HaveTree(root1.right, root2);
}
}
return result;
}
private boolean doesTree1HaveTree(TreeNode node1,TreeNode node2){
if (node2==null){
return true;
}
if (node1==null){
return false;
}
if (node1.val!=node2.val){
return false;
}
return doesTree1HaveTree(node1.left,node2.left)&&doesTree1HaveTree(node1.right,node2.right);
}
}
D.解决问题的思路
面试题 27:二叉树的镜像
/**
* 题目 27: 二叉树的镜像
* 请完成一个函数,输入一个二叉树,该函数输出它的镜像。
*
* 画图可以很清晰地得到思路:
* 先前序遍历,对每个结点交换左右子结点。
*/
public class D27 {
public class TreeNode{
int val=0;
TreeNode left=null;
TreeNode right=null;
public TreeNode(int val){
this.val=val;
}
public TreeNode(){}
}
public void mirrorRec(TreeNode root){
if (root==null){
return;
}
TreeNode tmp=root.left;
root.left=root.right;
root.right=tmp;
mirrorRec(root.left);
mirrorRec(root.right);
}
//前序遍历【根左右】的非递归镜像写法
//前序遍历只要某节点还有左子节点,就不断的压入栈中,直到没有左子节点时弹出
//接着将根节点指向右子树,重复上述过程
public void mirrorIterPre(TreeNode root){
LinkedList<TreeNode> stack=new LinkedList<>();
//当前节点不为空,或者栈有值,可以进行pop操作,都可以进入循环
while(root!=null || !stack.isEmpty()){
stack.push(root);
while(root!=null){
if (root.left!=null || root.right!=null){
TreeNode temp=root.left;
root.left=root.right;
root.right=temp;
}
root=root.left;
}
if (!stack.isEmpty()){
root=stack.pop();
root=root.right;
}
}
}
}
面试题 28:对称的二叉树
/**
* 题目 28: 对称的二叉树
* 请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和
* 的镜像一样,那么它是对称的。
*
* 还是画图分析,不用分析根结点,只需要分析左右子树。
* 可以看出,左右子树刚好是呈镜像的两颗二叉树,
* 所以:对左子树采用(父-左-右)的前序遍历,
* 右子树采用(父-右-左)的前序遍历,遍历时判断两个结点位置的值是否相等即可。
* (也可以这样理解:左树的左子树等于右树的右子树,左树的右子树等于右树的左子树,
* 对应位置刚好相反,判断两子树相反位置上的值是否相等即可)
*
*/
public class D28 {
public class TreeNode{
int val=0;
TreeNode left=null;
TreeNode right=null;
public TreeNode(int val){
this.val=val;
}
}
public boolean isSysmmentrical(TreeNode pRoot){
if (pRoot==null){
return true;
}
return isEqual(pRoot.left,pRoot.right);
}
private boolean isEqual(TreeNode pRoot1, TreeNode pRoot2) {
if (pRoot1==null && pRoot2==null){
return true;
}
if (pRoot1==null || pRoot2==null){
return false;
}
return pRoot1.val==pRoot2.val && isEqual(pRoot1.left, pRoot2.right)
&& isEqual(pRoot1.right, pRoot2.left);
}
}
面试题 29:顺时针打印矩阵
/**
* 题目 29:顺时针打印矩阵
* 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
*
* 思路:
* 把矩阵看成由若干个顺时针方向的圈组成,循环打印矩阵中的每个圈,每次循环打印一个圈。
* 打印一圈通常分为四步,
* 第一步从左到右打印一行;
* 第二步从上到下打印一列;
* 第三步从右到左打印一行;
* 第四步从下到上打印一列。
* 设置四个变量left,right,top,btm,用于表示圈的方位,
* 每一步根据起始坐标和终止坐标循环打印。
* 注意:
* 最后一圈有可能不需要四步,有可能只有一行,只有一列,只有一个数字,
* 因此要仔细分析打印每一步的前提条件
*/
public class D29 {
public static void printMatrixInCircle(int[][] numbers,int rows,int cols) {
if (numbers == null || cols < 0 || rows <= 0) {
return;
}
int start = 0;
int x = cols - 1 - start;
int y = rows - 1 - start;
//循环打印圈
/*
于是可以得出,让循环继续的条件是cols>startX×2并且rows>startY×2。所以可以用如下的循环来打印矩阵,
最大行为:x=rows-1-start;
最大列为:y=columns-1-start;
只需要确定4个顶点:(start,start),(start,y),(x,y),(x,start)。
然后在矩阵输出时判断好能输出的条件就行,
从左往右(start,start)到(start,y):判断矩阵存在后直接输出;
从上往下(start,y)到(x,y):需要x>start;
从右往左(x,y)到(x,start):需要x>start且y>start;
从下往上(x,start)到(start,start):需要x-1>start且y>start。
*/
while (cols>start*2 && rows>start*2){
//圈的第一步 从左到右打印一行
for (int i=start;i<=y;i++){
printNumber(numbers[start][i]);
}
//圈的第二步 从上到下打印一列
if (start<x){//至少有两行才需要进行第二步
for (int i=start+1;i<=x;i++){
printNumber(numbers[i][y]);
}
}
//圈的第三步 从右到左打印一行
if (start<x && start<y){//至少有两行两列才需要进行第三步
for (int i=y-1;i>=start;i--){
printNumber(numbers[x][i]);
}
}
//圈的第四步 从下到上打印一行
if (start<x-1 && start<y){
for (int i=x-1;i>start;i--){//至少有三行两列才需要进行第三步
printNumber(numbers[i][start]);
}
}
start++;
x--;
y--;
}
}
private static void printNumber(int number){
System.out.printf("%d\t",number);
}
}
面试题 30:包含min函数的栈
/**
* 题目 30: 包含min函数的栈
* 定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的min
* 函数。在该栈中,调用min、push及pop的时间复杂度都是O(1)。
*
* 最初想法是定义一个成员变量min来存放最小元素,但是当最小元素弹出后,min就需要相应改变,
* 所以必须把每次的最小值都存储下来。考虑采用一个辅助栈来存放最小值:
* 栈 3,4,2,5,1
* 辅助栈 3, 3,2,2,1
* (压入时,把每次的最小元素(之前最小元素与新入栈元素的较小值)保存起来放到辅助栈中)
*
*/
public class D30 {
private Stack<Integer> stack_date=new Stack<>();
private Stack<Integer> stack_min=new Stack<>();
public void push(int node){
stack_date.push(node);
if (stack_min.isEmpty() || stack_min.peek()>node){
stack_min.push(node);
}else {
stack_min.push(stack_min.peek());
}
}
public void pop(){
if (!stack_date.empty() && !stack_min.empty()){
stack_date.pop();
stack_min.pop();
}
}
}
面试题 31:栈的压入弹出
/**
* 题目 31: 栈的压入弹出
* 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是
* 否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1、2、3、4、
* 5是某栈的压栈序列,序列4、5、3、2、1是该压栈序列对应的一个弹出序列,但
* 4、3、5、1、2就不可能是该压栈序列的弹出序列。
*
* 思路
* 建立一个栈,按照压栈序列依次进行入栈操作,按出栈序列的顺序依次弹出数字。
* 在出栈时,若下一个要出栈的数字与栈顶数字相同则弹出。如果压栈序列中的所有数字都入栈后没有
* 完全出栈成功则代表两个序列不匹配,返回false。
*
*/
public class D31 {
public static boolean isPopOrder(int[] pushA,int[]popA){
if (pushA==null || popA==null){
return false;
}
if (popA.length!=pushA.length || pushA.length==0){
return false;
}
Stack<Integer> stack =new Stack<>();
int popIndex=0;
for (int pushIndex=0;pushIndex<pushA.length;pushIndex++){
stack.push(pushA[pushIndex]);
while(!stack.isEmpty() && stack.peek()==popA[popIndex]){
stack.pop();
popIndex++;
}
}
return stack.isEmpty();
}
}
面试题 32:从上到下打印二叉树
面试题 32(1):不分行从上到下打印二叉树
/**
* 题目 32(1):不分行从上到下打印二叉树
*
* 不分行从上往下打印二叉树:该题即为对二叉树的层序遍历,结点满足先进先出的原则,采用队列。
* 每从队列中取出头部结点并打印,若其有子结点,把子结点放入队列尾部,直到所有结点打印完毕。
*
*/
public class D32 {
public class TreeNode{
int val=0;
TreeNode left=null;
TreeNode right=null;
public TreeNode(int val){
this.val=val;
}
public TreeNode(){}
}
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
ArrayList<Integer> array=new ArrayList();
LinkedList<TreeNode> queue=new LinkedList<>();
if (root==null){
return array;
}
queue.offer(root);
TreeNode node=null;
while(queue.size()!=0){
node=queue.poll();
array.add(node.val);
if (node.left!=null){
queue.offer(node.left);
}
if (node.right!=null){
queue.offer(node.right);
}
}
return array;
}
}
面试题 32(2):分行从上到下打印二叉树
/**
* 题目 32(2):分行从上到下打印二叉树
* 从上到下按层打印二叉树,同一层的结点按从左到右的顺序打印,
* 每一层打印到一行。
*
* 同样使用队列,但比第一题增加两个变量:当前层结点数目pCount,下一层结点数目nextCount。
* 根据当前层结点数目来打印当前层结点,同时计算下一层结点数目,之后令pCount等于nextCount,
* 重复循环,直到打印完毕。
*/
public class D321 {
public class TreeNode{
int val=0;
TreeNode left=null;
TreeNode right=null;
public TreeNode(int val){
this.val=val;
}
public TreeNode(){}
}
public void print(TreeNode pRoot){
printTreeNode(pRoot);
}
private void printTreeNode(TreeNode root) {
if(root==null){
return;
}
LinkedList<TreeNode> queue=new LinkedList<>();
queue.offer(root);
TreeNode node=null;
int pCount=0;
int nextCount=1;
while(!queue.isEmpty()){
pCount=nextCount;
nextCount=0;
for (int i=0;i<pCount;i++){
node=queue.poll();
System.out.print(node.val+" ");
if (node.left!=null){
queue.offer(node.left);
nextCount++;
}
if (node.right!=null){
queue.offer(node.right);
nextCount++;
}
}
System.out.println();
}
}
}
面试题 32(3):之字形打印二叉树
/**
* 题目 32(3): 之字形打印二叉树
* 请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺
* 序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,
* 其他行以此类推。
*
* 思路:
* 采用两个栈,对于不同层的结点,一个栈用于正向存储,一个栈用于逆向存储,打印出来就正好是相反方向。
*/
public class D322 {
public class TreeNode{
int val=0;
TreeNode left=null;
TreeNode right=null;
public TreeNode(int val){
this.val=val;
}
public TreeNode(){}
}
public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer> > result=new ArrayList<ArrayList<Integer>>();
ArrayList<Integer> array1=new ArrayList<Integer>();
ArrayList<Integer> array2=new ArrayList<Integer>();
if (pRoot==null){
return result;
}
Stack<TreeNode> stack1=new Stack<>();
Stack<TreeNode> stack2=new Stack<>();
TreeNode node=null;
stack1.push(pRoot);
while(!stack1.isEmpty() || !stack2.isEmpty()){
array1=new ArrayList<Integer>();
array2=new ArrayList<Integer>();
//父亲所处奇数层
while(!stack1.isEmpty()){
node=stack1.pop();
array1.add(node.val);
if (node.left!=null){
stack2.push(node.left);
}
if (node.right!=null){
stack2.push(node.right);
}
}
//父亲所处偶数层
while (!stack2.isEmpty()){
node=stack2.pop();
array2.add(node.val);
if (node.right!=null){
stack1.push(node.right);
}
if (node.left!=null){
stack1.push(node.left);
}
}
if(!array1.isEmpty()){
result.add(array1);
}
if(!array2.isEmpty()){
result.add(array2);
}
}
return result;
}
}
面试题 33: 二叉搜索树的后序遍历序列
/**
* 题目 33: 二叉搜索树的后序遍历序列
* 如果是则返回true,否则返回false。假设输入的数组的任意两个数字都互不相同。
*
* 思路:
* 二叉树后序遍历数组的最后一个数为根结点,剩余数字中,小于根结点的数字(即左子树部分)都排在前面,
* 大于根结点的数字(即右子树部分)都排在后面。根据遍历数组的这个特性,可以编写出一个递归函数,
* 用于实现题目所要求的判断功能。
*/
public class D33 {
public boolean verifySequenceOfBST(int[] sequence){
if (sequence==null || sequence.length<=0){
return false;
}
return verifyCore(sequence,0,sequence.length-1);
}
private boolean verifyCore(int[] sequence, int start, int end) {
if (start>=end){
return true;
}
//判断左子树
int mid=start;
//由于左子树都比root小
while (sequence[mid]<sequence[end]){
mid++;
}
//出了循环就找到了左右子树的边界 mid
//判断右子树
for (int i=mid;i<end;i++){
//如果右子树中还存在比root小的情况,那么就是说明该树不是二分搜索树
if (sequence[i]<sequence[end]){
return false;
}
}
return verifyCore(sequence, start, mid-1)
&& verifyCore(sequence,mid, end-1);
}
}
面试题 34:二叉树中和为某一值的路径
/**
* 题目 34 :二叉树中和为某一值的路径
* 输入一棵二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所
* 有路径。从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
*
* 思路:
* 1.假设找到了其中一条路径,达到叶结点后,由于没有指向父结点的指针,所以必须提前创建一个链表存储前面经过的结点。
*
* 2.由于是从根结点出发,所以要想到使用前序遍历
*
* 3.利用链表存储结点,在该结点完成左右子树的路径搜索后(即递归函数结束,返回到其父结点之前),要删除该结点,从而记录别的路径。
*
* 具体实现:通过前序遍历,从根结点出发,每次在链表中存储遍历到的结点,若到达叶子结点,则根据所有结点的和是否等于输入的整数,
* 判断是否打印输出。在当前结点访问结束后,递归函数将会返回到它的父结点,所以在函数退出之前,要删除链表中的当前结点,
* 以确保返回父结点时,储存的路径刚好是从根结点到父结点。
*
* 改进:书中的代码是根据所有结点的和是否等于输入的整数,判断是否打印输出。其实没有这个必要,只需要在每次遍历到一个结点时,
* 令目标整数等于自己减去当前结点的值,若到达根结点时,最终的目标整数等于0就可以打印输出。(相当于每个结点的目标整数不同,详见代码)
*/
public class D34 {
public class TreeNode{
int val=0;
TreeNode left=null;
TreeNode right=null;
public TreeNode(){
}
public TreeNode(int val){
this.val=val;
}
}
ArrayList<ArrayList<Integer>> result=new ArrayList();
ArrayList<Integer> list=new ArrayList<>();
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
if(root==null){
return result;
}
list.add(root.val);
target-=root.val;
if (target==0 && root.left==null && root.right==null){
result.add(new ArrayList<Integer>(list));
}else {
FindPath(root.left,target);
FindPath(root.right,target);
}
list.remove(list.size()-1);
return result;
}
}
面试题 35:复杂链表的复制
/**
* 题目 35: 复杂链表的复制
* 请实现函数ComplexListNode* Clone(ComplexListNode* pHead),复
* 制一个复杂链表。在复杂链表中,每个结点除了有一个m_pNext指针指向下一个
* 结点外,还有一个m_pSibling 指向链表中的任意结点或者nullptr。
*
* 思路
* 思路1:先复制结点,用next链接,最后根据原始结点的sibling指针确定该sibling结点距离头结点的位置,
* 从而对复制结点设置sibling指针。但是该思路对于n个结点的链表,每个结点的sibling都需要O(n)个时间步才能找到,
* 所以时间复杂度为O(n^2)
*
* 思路2:复制原始结点N创建N’,用next链接。将<N,N'>的配对信息存放入一个哈希表中;在设置sibling时,通过哈希表,
* 只需要用O(1)的时间即可找到复制结点的sibling。该方法的时间复杂度为O(n),但空间复杂度为O(n)。
*
* 思路3:复制原始结点N创建N’,将N'链接到N的后面;根据原始结点N的sibling可以快速设置N'结点的sibling,
* 最后将这个长链表拆分成原始链表和复制链表(根据奇偶位置)
*/
public class D35 {
public class ComplexListNode{
String val;//2' 3'
ComplexListNode sibling=null;
ComplexListNode next=null;
ComplexListNode(String val){
this.val=val;
}
ComplexListNode(){}
}
//主程序 包含三步
public ComplexListNode cloneList(ComplexListNode head){
cloneNodes(head);//第一步 复制节点
connectionSiblingNodes(head);// 第二步 设置任意指定指针
return reconnectNodes(head);/// 第三步 拆分长链表,得到克隆后的链表
}
//第一步 复制节点
private void cloneNodes(ComplexListNode head) {
ComplexListNode pNode=head;
while(pNode!=null){
ComplexListNode cloneNode=new ComplexListNode(pNode.val+"’");
cloneNode.next=pNode.next;
pNode.next=cloneNode;//形成了 原始节点A->复制后的节点A'->原始节点B->府之后的节点B’
pNode=cloneNode.next;// 从A节点到了B节点
}
}
//第二步 设置节点任意指定指针
private void connectionSiblingNodes(ComplexListNode head) {
ComplexListNode pNode=head;
while(pNode!=null){
if (pNode.sibling!=null){
pNode.next.sibling=pNode.sibling.next;
}
pNode=pNode.next.next;
}
}
//第三步 拆分长链表,得到克隆后的链表[根据奇数[原始链表]偶数[克隆后的链表]位置
private ComplexListNode reconnectNodes(ComplexListNode head) {
ComplexListNode cloneHead=null;
ComplexListNode cloneNode=null;
ComplexListNode pNode=head;
if (head!=null){
cloneHead=head.next;
cloneNode=head.next;
pNode.next=cloneNode.next;
pNode=pNode.next;
}
while(pNode!=null){
cloneNode.next=pNode.next;
cloneNode=cloneNode.next;
pNode.next=cloneNode.next;
pNode=pNode.next;
}
return cloneHead;
}
}
面试题 36:二叉搜索树与双向链表
/**
* 题目 36: 二叉搜索树与双向链表
* 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求
* 不能创建任何新的结点,只能调整树中结点指针的指向。
*
* 思路:
* 二叉搜索树、排序链表,想到使用中序遍历。
* 要实现双向链表,必须知道当前结点的前一个结点。根据中序遍历可以知道,当遍历到根结点的时候,
* 左子树已经转化成了一个排序的链表了,根结点的前一结点就是该链表的最后一个结点(这个结点必须记录下来,将遍历函数的返回值
* 设置为该结点即可),链接根结点和前一个结点,此时链表最后一个结点就是根结点了。再处理右子树,遍历右子树,将右子树的最小结
* 点与根结点链接起来即可。左右子树的转化采用递归即可。
* 大概思想再理一下:首先想一下中序遍历的大概代码结构(先处理左子树,再处理根结点,之后处理右子树),假设左子树处理完了
* ,就要处理根结点,而根结点必须知道左子树的最大结点,所以要用函数返回值记录下来;之后处理右子树,右子树的最小结点(
* 也用中序遍历得到)要和根结点链接。
* 注意搞清楚修改后的中序遍历函数的意义(见代码注释)
*/
public class D36 {
public class TreeNode{
int val=0;
TreeNode left=null;
TreeNode right=null;
public TreeNode(int val){
this.val=val;
}
public TreeNode(){}
}
public TreeNode Convert(TreeNode pRootOfTree) {
if(pRootOfTree==null){
return pRootOfTree;
}
TreeNode lastNode=null;
TreeNode firstNode=convertCore(pRootOfTree,lastNode);
while(firstNode.left!=null){
firstNode=firstNode.left;
}
return firstNode;
}
/**
*中序遍历,返回当前遍历的节点
*/
private TreeNode convertCore(TreeNode root,TreeNode lastNode) {
if(root==null){//退出递归条件
return lastNode;
}
TreeNode currentNode=root;
if(currentNode.left!=null){//左
lastNode=convertCore(currentNode.left,lastNode);
}
currentNode.left=lastNode;
if(lastNode!=null){
lastNode.right=currentNode;
}
lastNode=currentNode;//根
if(currentNode.right!=null){//右
lastNode=convertCore(currentNode.right,lastNode);
}
return lastNode;
}
}
面试题 37:序列化二叉树
/**
* 题目 37:序列化二叉树
* 请实现两个函数,分别用来序列化和反序列化二叉树
*/
public class D37 {
private class TreeNode{
int val=0;
TreeNode left=null;
TreeNode right=null;
public TreeNode(int val){
this.val=val;
}
public TreeNode(){
}
}
public String serialize(TreeNode node){
StringBuilder sb=new StringBuilder();
if (node==null){
sb.append("$,");
}else {
sb.append(node.val+",");
sb.append(serialize(node.left));
sb.append(serialize(node.right));
}
return sb.toString();
}
private int index=0;
public TreeNode deserialize(String str) {//反序列化 前序遍历 根左右
TreeNode node=null;
if (str==null || str.length()==0){
return null;
}
int start=index;
while(str.charAt(index)!=','){
index++;
}
if (!str.substring(start,index).equals("$")){
node=new TreeNode(Integer.parseInt(str.substring(start,index)));
index++;
node.left=deserialize(str);
node.right=deserialize(str);
}else {
index++;
}
return node;
}
}
面试题 38:字符串的排列
/**
* 题目 38: 字符串的排列
* 输入一个字符串,打印出该字符串中字符的所有排列。例如输入字符串abc,
* 则打印出由字符a、b、c所能排列出来的所有字符串abc、acb、bac、bca、cab和cba。
* 将字符串看成两部分,一部分是第一个字符,另一部分是后面的所有字符。
* 首先确定第一个字符,该字符可以是字符串中的任意一个;固定第一个字符后,求出后面所有字符的排列(相同步骤,采用递归)。
*
*/
public class D38 {
public ArrayList<String> permutation(String str){
ArrayList<String> list=new ArrayList<>();
if (str==null || str.length()==0){
return list;
}
permutationCore(str.toCharArray(),1,list);
Collections.sort(list);
return list;
}
private void permutationCore(char[] strArray, int index, ArrayList<String> list) {
if (index==strArray.length-1){//字符串排列完毕
list.add(String.valueOf(strArray));// a bc /a cb /b ac /b ca /c ab /c ba
}else {
for (int i=index;i<strArray.length;i++){
char temp=strArray[index];//b和c交换
strArray[index]=strArray[i];
strArray[i]=temp;
permutationCore(strArray, index+1, list);
//退出递归执行的语句 第一种排列完事了,需要在回到原始位置,
// 为下一次b和a做交换打基础 此时的 index值是1
temp=strArray[index];
strArray[index]=strArray[i];
strArray[i]=temp;
}
}
}
}
E.优化时间和空间效率
面试题 39:数组中出现次数超过一半的数字
/**
* 题目 39:数组中出现次数超过一半的数字
* 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例
* 如输入一个长度为9的数组{1, 2, 3, 2, 2, 2, 5, 4, 2}。由于数字2在数组中
* 出现了5次,超过数组长度的一半,因此输出2。
*/
public class E39 {
public int MoreThanHalfNum_Solution(int [] array) {
HashMap<Integer,Integer> hm=new HashMap();
for(int i=0;i<array.length;i++){
if(hm.containsKey(array[i])){
hm.put(array[i],hm.get(array[i])+1);
}else{
hm.put(array[i],1);
}
}
for(Entry<Integer,Integer> entry:hm.entrySet()){
if(entry.getValue()>array.length>>1){
return entry.getKey();
}
}
return 0;
}
}
面试题 40:最小的k个数
/**
* 题目 40:最小的k个数
* 输入n个整数,找出其中最小的k个数。例如输入4、5、1、6、2、7、3、8
* 这8个数字,则最小的4个数字是1、2、3、4。
*
* 思路:依次遍历n个整数,用一个容器存放最小的k个数字,每遇到比容器中最大的数字还小的数字时,
* 将最大值替换为该数字。容器可以使用大顶堆或者红黑树来实现。本文根据堆排序的原理来实现。
*
* 有一个很简单的想法:设想有一个容器,其大小不超过k时,就不断存入元素,只要容量超过k,就剔除其中最大的元素,重复该过程,
* 当遍历所有元素后,该容器中剩下的刚好就是最小的k个元素。 *
* 哪种容器能快速得到最大元素,可能第一想到的就是最大堆。 *
* 面试了哪有那么多时间让你手写一个堆和一大堆代码的切分方法。而且上述两种方法都改变了原数组。
* Java内置了优先队列,就是基于堆实现的,默认是最小堆,可以传入Comparator改变堆的形式。
*/
public class E40 {
public ArrayList<Integer> GetLeastNumbers(int[] input,int k){
ArrayList<Integer> list=new ArrayList<>();
if (input==null || input.length==0 || k>input.length || k<0){
return list;
}
PriorityQueue<Integer> maxHeap=new PriorityQueue(Comparator.reverseOrder());
for (int i:input){
maxHeap.offer(i);
if (maxHeap.size()>k){
maxHeap.poll();
}
}
list.addAll(maxHeap);
return list;
}
}
面试题 41:数据流中的中位数
/**
* 题目 41:数据流中的中位数
*
*/
public class E41 {
private PriorityQueue<Integer> minHeap=new PriorityQueue<>();
private PriorityQueue<Integer> maxHeap=new PriorityQueue<>(Comparator.reverseOrder());
public void insert(Integer num){
if (((minHeap.size()+maxHeap.size())&1)==0){//偶数个,放入小顶堆
//由于个数已经是偶数了,要想保证数列是平衡的,要求小顶堆的个数大于大顶堆的个数为1个,因此新数将
//插入到小顶堆中,如果新数比大顶堆最大的数还小,因此就不能够插入到小顶堆中了,因为小顶堆的数比大顶堆的最大数还大
//先插入到大顶堆中,再把大顶堆中的根元素取出来,放入到小顶堆中
if (!maxHeap.isEmpty() && maxHeap.peek()>num){
maxHeap.offer(num);
num=maxHeap.poll();
}
minHeap.offer(num);
}else {
//如果新数比大顶堆的根元素还大,那么就不能放到大顶堆中了,我们需要把它放到小顶堆中,但是超过了平衡
// 【小顶堆的数据要大于大顶堆数据1个】,因此先把新数放到小顶堆中,再把小顶堆的根元素取出来,放到大顶堆中。
if(!minHeap.isEmpty()&&minHeap.peek()<num){
minHeap.offer(num);
num=minHeap.poll();
}
maxHeap.offer(num);
}
}
public Integer getMedian(){//取出中间数
if (minHeap.size()+maxHeap.size()==0){
throw new RuntimeException();
}
Integer m;
if(((minHeap.size()+maxHeap.size())&1)==0){//数列是偶数个元素
int max=maxHeap.peek();
int min=minHeap.peek();
m=(max+min)>>1;//中间数
}else{
m=minHeap.peek();//中间数
}
return m;
}
}
面试题 42:连续子数组最大和
/**
* 题目 42:连续子数组最大和
* 输入一个整型数组,数组里有正数也有负数。数组中一个或连续的多个整
* 数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。
*
* 思路:
* 分析规律,从第一个数字开始累加,若走到某一个数字时,前面的累加和为负数,
* 说明不能继续累加了,要从当前数字重新开始累加。在累加过程中,将每次累加和的最大值记录下来,遍历完成后,返回该数字。
*/
public class E42 {
boolean invalidInput=false;
private int findGreatestSumOfSubarray(int [] array){
if (array==null || array.length<=0){
invalidInput=true;
return 0;
}
invalidInput=false;
int sum=array[0];
int maxSum=array[0];
for (int i=1; i<array.length;i++){
if (sum<0){
sum=array[i];
}else {
sum+=array[i];
}
if (sum>maxSum){
maxSum=sum;
}
}
return maxSum;
}
}
面试题 43:1 ~ n 整数中 1 出现的次数
/**
* 题目 43: 1 ~ n 整数中 1 出现的次数
* 输入一个整数n,求从1到n这n个整数的十进制表示中1出现的次数。例如
* 输入12,从1到12这些整数中包含1 的数字有1,10,11和12,1一共出现了5次。
*
* * 思路
* * 如果是从头到尾遍历(n次),对每一个数字都计算其1的个数(lgn次),则时间复杂度为O(nlogn),运算效率太低。因此必须总结规律,提高效率。 *
* * 总结规律如下(思路比《剑指OFFER》一书简单): *
* * 对于整数n,我们将这个整数分为三部分:当前位数字cur,更高位数字high,更低位数字low,如:对于n=21034,当位数是十位时,
* * cur=3,high=210,low=4。 *
* * 我们从个位到最高位 依次计算每个位置出现1的次数: *
* * 1)当前位的数字等于0时,例如n=21034,在百位上的数字cur=0,百位上是1的情况有:00100~00199,01100~01199,……,20100~20199。
* * 一共有21*100种情况,即high*100; *
* * 2)当前位的数字等于1时,例如n=21034,在千位上的数字cur=1,千位上是1的情况有:01000~01999,11000~11999,21000~21034。
* * 一共有2*1000+(34+1)种情况,即high*1000+(low+1)。 *
* * 3)当前位的数字大于1时,例如n=21034,在十位上的数字cur=3,十位上是1的情况有:00010~00019,……,21010~21019。
* * 一共有(210+1)*10种情况,即(high+1)*10。
* * 这个方法只需要遍历每个位数,对于整数n,其位数一共有lgn个,所以时间复杂度为O(logn)。
*/
public class E43 {
public int numberOfBetweenl(int n){
int count=0;
for (int i=1;i<=n;i*=10){//i代表X分位 21034 high[2103] cur[4] low[0]
int high=n/(i*10);
int low=n%i;
int cur=(n/i)%10;
if (cur==0){
count+=high*i;
}else if (cur==1){
count+=high*i+(low+1);
}else {
count+=(high+1)*i;
}
}
return count;
}
}
面试题 44:数字序列中某一位的数字
/**
* 题目 44: 数字序列中某一位的数字
* 数字以0123456789101112131415.......的格式序列化到一个字符序列中。在这个序列中,第5位(从0开始计数)是5,第13位是1,
* 第19位是4,等等。 * 请写一个函数,求任意第n位对应的数字。
*
* * 思路:
* * 从数字本身的规律入手。如果要找第1001位,肯定不会考虑0-9十位吧?接下来10-99也就有180位而已,不考虑,
* * 每次不考虑的情况需要减去已缩小查找范围,因此在排除掉0-99共180+10后还剩881,也就是说我们原来是从0开始找到第1001位,
* * 现在只需从100开始找到第991位即可。接下来看100-999共2700位,由于881 < 2700,所以这个数必然在100-999的范围内。
* * 881 = 270 * 3 + 1,说明这个数是100开始之后的第270个数的第1位(从0开始计算索引),也就是370的第一位数,即7。
*/
public class E44 {
public int digitAtIndex(int index) {
if(index<0)
return -1;
int m=1; //m位数
while(true) {
int numbers=numbersOfIntegers(m); //m位数的个数
if(index<numbers*m){
return getDigit(index,m);
}
index-=numbers*m;
m++;
}
}
/*
* 返回m位数的总个数
* 例如,两位数一共有90个:10~99;三位数有900个:100~999
*/
private int numbersOfIntegers(int m) {
if(m==1)
return 10;
return (int) (9*Math.pow(10, m-1));
}
/*
* 获取数字
*/
private int getDigit(int index, int m) {
int number=getFirstNumber(m)+index/m; //对应的m位数
int indexFromRight = m-index%m; //在数字中的位置
for(int i=1;i<indexFromRight;i++){
number/=10;
}
return number%10;
}
/*
* 第一个m位数
* 例如第一个两位数是10,第一个三位数是100
*/
private int getFirstNumber(int m) {
if(m==1)
return 0;
return (int) Math.pow(10, m-1);
}
}
面试题 45:把数组排成最小的数
/**
* 题目 45: 把数组排成最小的数
* 输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼
* 接出的所有数字中最小的一个。例如输入数组{3, 32, 321},则打印出这3个数
* 字能排成的最小数字321323。
*
* 不好的方法:求出所有全排列(类似字符串的排列 ),将数字拼起来,最后求出所有的最小值。这效率太低,且没有考虑到大数问题。 *
* 好的方法:观察规律,自行定义一种排序规则。
*
* 对于数字m和n,可以拼接成mn和nm,如果mn<nm,我们定义m小于n。反之则相反。利用这个排序规则,从小排到大即可实现题目要求。 *
* 拼接m和n时,要考虑到大数问题,因此将m和n拼接起来的数字转换成字符串处理。因为mn和nm的字符串位数相同,因此它们的大小只
* 需要按照字符串大小的比较规则就可以了。
* 具体实现:将数字存入ArrayList中,通过利用Collections.sort(List<T> list, Comparator<? super T> c)方法进行排序。
* Comparator中重写compar()方法来规定比较规则。
*/
public class E45 {
public String printMinNumber(int [] numbers){
if(numbers==null || numbers.length<=0){
return "";
}
ArrayList<String> list=new ArrayList<>();
for (int number: numbers){
list.add(String.valueOf(number));
}
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
String a=o1+o2;
String b=o2+o1;
return a.compareTo(b);
}
});
StringBuffer sb=new StringBuffer();
for (String str: list){
sb.append(str);
}
return sb.toString();
}
}
面试题 46:把数字翻译成字符串
/**
* 题目 46: 把数字翻译成字符串
* 给定一个数字,我们按照如下规则把它翻译为字符串:0翻译成"a",1翻
* 译成"b",……,11翻译成"l",……,25翻译成"z"。一个数字可能有多个翻译。例
* 如12258有5种不同的翻译,它们分别是"bccfi"、"bwfi"、"bczi"、"mcfi"和
* "mzi"。请编程实现一个函数用来计算一个数字有多少种不同的翻译方法。
*
* 如果自下而上,从小的结果出发,保存每一步计算的结果,以供下一步使用,也就是我们按照从右到左的顺序翻译。
* f(i) = f(i+1) + g(i,i+1)*f(i+2),
* i和i+1位数字拼起来在10~25范围内时g(i,i+1)的值为1,否则为0。
* 对于上面的公式,也就是先求出f(len -1),然后求出f(len -2),之后根据这两个值求出f(len -3),
* 然后根据f(len-2)和f(len -3)求出f(len -4)一直往左知道求出f(0),这就是我们要的结果。
*/
public class E46 {
public int getTranslationCount(int n){
if(n<0){
return 0;
}
return count(String.valueOf(n));
}
private int count(String num) {
int len=num.length();
int []counts=new int[len];
counts[len-1]=1;
for (int i=len-2;i>0;i--){
int high=num.charAt(i)-'0';
int low=num.charAt(i+1)-'0';
int combineNum=high*10+low;
if (combineNum>=10 && combineNum<=25){
if (i==len-2){
counts[i]=counts[i+1]+1;
}else{
counts[i]=counts[i+1]+counts[i+2];
}
}else {
counts[i]=counts[i+1];
}
}
return counts[0];
}
}
面试题 47:礼物的最大值
/**
* 题目 47: 礼物的最大值
* 在一个m×n的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值
* (价值大于0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向左或
* 者向下移动一格直到到达棋盘的右下角。给定一个棋盘及其上面的礼物,请计
* 算你最多能拿到多少价值的礼物?
*
* 动态规划:定义f(i,j)为到达(i,j)位置格子时能拿到的礼物总和的最大值,
* 则有:f(i,j)=max{f(i-1,j),f(i,j-1)}+values(i,j)。
* 同上道题一样,如果直接使用递归会产生大量的重复计算,因此,创建辅助的数组来保存中间计算结果。
*
*/
public class E47 {
public int maxValueOfGifts(int [][] values){
if(values==null||values.length<=0||values[0].length<=0){
return 0;
}
int rows=values.length;
int cols=values[0].length;
int [][] maxValue=new int[rows][cols];
for(int i=0;i<rows;i++){
for (int j=0;j<cols;j++){
int left=0;
int above=0;
if (i>0){
above=maxValue[i-1][j];
}
if (j>0){
left=maxValue[i][j-1];
}
maxValue[i][j]=Math.max(above,left)+values[i][j];
}
}
return maxValue[rows-1][cols-1];
}
}
面试题 48:最长不包含重复字符的子字符串
/***
* 题目 48: 最长不包含重复字符的子字符串
* 请从字符串中找出一个最长不包含重复字符的子字符串,
* 计算该最长子字符串的长度。假设字符串只包含'a'~'z'的字符。
* 例如,在字符串"arabcacfr"中,最长的不包含重复字符的子字符串是"acfr",长度是4
*
* 动态规划,
* 定义f(i)表示以第i个字符为结尾的不含重复字符的子字符串长度。
* 定义f(i -1)表示以第i-1个字符为结尾的不含重复字符的子字符串长度。
* 如果第i个字符之前没有出现过,则f(i) = f(i -1) +1,比如‘abc',f(0) = 1是必然的,
* 再字符'b'之前没有出现过, 则f(1) = f(0)+1,
* 字符’c'之前没有出现过,那么f(2) = f(1) +1,每次计算都会用到上一次计算的结果。
* 如果第i个字符之前出现过呢?找到该字符上次出现的位置preIndex,当前位置d=i-preIndex就得到这两个重复字符之间的距离, 设为d。
* 此时有两种情况:
* 如果d <= f(i-1),说明当前重复字符必然在f(i-1)所对应的字符串中,比如'bdcefgc',当前字符c前面出现过了,preIndex为2,
* 此时d = 6 -2 = 4 ,小于 f(i -1) = 6 (bdcefg)我们只好丢弃前一次出现的字符c及其前面的所有字符,得到当前最长不含重复字符的子
* 字符串为’efgc‘,即f(i) = 4, 多举几个例子就知道,应该让f(i) = d;
* 如果d > f(i-1), 这说明当前重复字符必然出现在f(i-1)所对应的字符串之前,比如erabcdabr当前字符r和索引1处的r重复,
* preIndex =1, i = 8,d=(i-preIndex) = 7。而f(i -1) = 4 (取索引4之后不包含重复字符的串cdab),而7>4,
* 此时直接加1即可, 即f(i) = f(i-1) +1
*
*/
public class E48 {
/**
*
* @param str 测试的字符串
* @return 最长不含重复子字符串的长度
*/
private int findLongestSubstring(String str){
int curLen=0;//当前长度
int maxLen=0;//最大长度
int []position=new int[26];//某个字符曾经出现在index处
for(int i=0;i<26;i++){
position[i]=-1;//默认值是-1 代表都没出现过
}
for(int i=0;i<str.length();i++){
char t=str.charAt(i);
int preIndex=position[t-'a'];//是否该字符曾经出现过
int d=i-preIndex;//代表的是现在的索引 和曾经出现的索引的距离,
/** 如果d > f(i-1), 这说明当前重复字符必然出现在f(i-1)所对应的字符串之前,
* 比如erabcdabr当前字符r和索引1处的r重复,
* preIndex =1, i = 8,d=(i-preIndex) = 7。而f(i -1) = 4 (取索引4之后不包含重复字符的串cdab),而7>4,
* 此时直接加1即可, 即f(i) = f(i-1) +1
*/
if(preIndex==-1||d>curLen){
curLen++;
}else{//preIndex!=-1&& d<=curLen
/** 如果d <= f(i-1),说明当前重复字符必然在f(i-1)所对应的字符串中,
* 比如'bdcefgc',当前字符c前面出现过了,preIndex为2,
* 此时d = 6 -2 = 4 ,小于 f(i -1) = 6 (bdcefg)
* 只好丢弃前一次出现的字符c及其前面的所有字符,得到当前最长不含重复字符的子
* 字符串为’efgc‘,即f(i) = 4, 多举几个例子就知道,应该让f(i) = d;
*/
curLen=d;
}
position[str.charAt(i)-'a']=i;
if(curLen>maxLen){
maxLen=curLen;
}
}
return maxLen;
}
}
面试题 49:丑数
/**
* 题目 49: 丑数
* 把只包含因子2、3和5的数称作丑数(Ugly Number)。
* 例如6、8都是丑数,但14不是,因为它包含因子7。
* 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
* 求按从小到大的顺序找到第1500个丑数是多少
*/
public class E49 {
/**
* 逐个判断每个整数是不是丑数的解法,直观但不够高效
*/
/**
*
* @param index 求第XX个丑数
* @return 那个丑数是多少
*/
private int getUglyNumberSo1(int index){
if(index<=0){
return 0;
}
int number=0;//算出index个的那个丑数是多少
int uglyCount=0;//丑数的累加器
while(uglyCount<index){
++number;
if(isUgly(number)){
++uglyCount;
}
}
return number;
}
/**
*
* @param number 判断的一个数
* @return true:是 false:不是
*/
private boolean isUgly(int number) {
while(number%2==0){
number/=2;
}
while(number%3==0){
number/=3;
}
while(number%5==0){
number/=5;
}
return number==1?true:false;
}
/**
* 有没有方法每一步计算只是得到丑数呢?根据丑数的定义,所有丑数都是2、3、5这三个因子的任意搭配的任意多次乘积,
* 比如2x2,2x3, 2x2x3x5等等。
* 那么从1开始,分别乘以2、3、5,得到2、3、5三个丑数,但是这并不是正确的排序,我们知道3、5之间
* 还有个4也是丑数。1之后的下一个丑数,一定是2、3、5其中的一个,显然应该选三者中最小的2,现在丑数集合为{1, 2}且有序,
* 刚才选择的2是1x2得到的,因此下一个和2相乘的丑数应该是1之后的数字2(丑数集合已经有序,直接选择下一个)。
* 现在又得到三个候选的丑数4、3、5,
* 再次选择三者中最小的3,得到当前丑数集合{1, 2, 3},刚被选走的3由1x3得到,
* 因此下一个要和3相乘的按照丑数集合的顺序应该是2,然后又得到了三个候选的丑数4、6、5,选择最小的4.....不断重复,
* 自始至终只和丑数打交道。
* 设定三个数t2、t3、t3专门用于分别和2、3、5相乘,某次选择中选走了ti,那么ti从丑数集合中选择下一个数,下次再和i相乘生成一个
* 新的候选丑数,本次没有被选中的,下次继续参与比较。这样能保证下一个丑数一定在三个候选项中,且是三个候选项中最小的那个。
*/
public int getUglyNumberSo2(int index) {
if (index <= 0) {
return 0;
}
int t2 = 0;//*2 后 在操作丑数数组的索引
int t3 = 0;//*3 后 在操作丑数数组的索引
int t5 = 0;//*5 后 在操作丑数数组的索引
int[] res = new int[index];
//第一个丑数为1
res[0] = 1;
for (int i = 1; i < index; i++) {//每个t2 t3 t5都自增在分别乘以 2 3 5得出新的丑数
//进行最小丑数的选值,把小的丑数存入到res数组中
int m2 = res[t2] * 2;
int m3 = res[t3] * 3;
int m5 = res[t5] * 5;
//找出新旧三个丑数的最小丑数
res[i] = Math.min(m2, Math.min(m3, m5));
if (res[i] == m2) {////对res的索引t2进行自增 得出新丑数的索引号
t2++;
}
if (res[i] == m3) {////对res的索引t3进行自增 得出新丑数的索引号
t3++;
}
if (res[i] == m5) {////对res的索引t5进行自增 得出新丑数的索引号
t5++;
}
}
return res[index - 1];//因为从零号开始存储的是第一个丑数
}
}
面试题 50:第一个只出现一次的字符
面试题 50(1):字符串中第一个只出现一次的字符
/**
* 题目 50:(1):字符串中第一个只出现一次的字符
* 如输入"abaccdeff",则输出'b'
*
* 思路
* 创建哈希表,键值key为字符,值value为出现次数。第一遍扫描:对每个扫描到的字符的次数加一;第二遍扫描:
* 对每个扫描到的字符通过哈希表查询次数,第一个次数为1的字符即为符合要求的输出。 *
* 由于字符(char)是长度为8的数据类型,共有256中可能,因此哈希表可以用一个长度为256的数组来代替,
* 数组的下标相当于键值key,对应字符的ASCII码值;数组的值相当于哈希表的值value,用于存放对应字符出现的次数。
*/
public class E50 {
public char firstNoRepeatringChar(String str){
if(str==null){
return '\0';
}
int [] repetitions=new int[256];
//拆解字符串为每个字符,判断是否有相同,有就在数组中ascii码指定的索引中做累加
for(int i=0;i<str.length();i++){
int loc=str.charAt(i);
repetitions[loc]+=1;
}
for(int i=0;i<str.length();i++){
int loc=str.charAt(i);
if(repetitions[loc]==1){//仅出现一次
return (char)loc;
}
}
return '\0';//都出现了多次 的返回
}
}
面试题 50(2):字符流中第一个只出现一次的字符
/**
* 题目 50(2):字符流中第一个只出现一次的字符
* 请实现一个函数用来找出字符流中第一个[按ascii码来确定先后]只出现一次的字符。例如,当从
* 字符流中只读出前两个字符"go"时,第一个只出现一次的字符是'g'。当从该字
* 符流中读出前六个字符"google"时,第一个只出现一次的字符是'l'。
* 思路
* 字符只能一个一个从字符流中读出来,因此要定义一个容器来保存字符以及其在字符流中的位置。 *
* 为尽可能解决问题,要在O(1)时间内往数据容器中插入字符,及其对应的位置,因此这个数据容器可以用哈希表来实现,
* 以字符的ASCII码作为哈希表的键值key,字符对应的位置作为哈希表的值value。 *
* 开始时,哈希表的值都初始化为-1,当读取到某个字符时,将位置存入value中,如果之前读取过该字符(即value>=0),
* 将value赋值为-2,代表重复出现过。最后对哈希表遍历,在value>=0的键值对中找到最小的value,
* 该value即为第一个只出现一次的字符,ASCII码为key的字符即为所求字符。
*/
public class E501 {
private int[] occur = new int[256];
private int index = 1;
public void Insert(char ch) {
if (occur[ch] == 0) {
occur[ch] = index;
} else {
occur[ch] = -2;
}
index++;
}
public char FirstAppearingOnce() {
char ch = '#';
int min = Integer.MAX_VALUE;
for (int i = 0; i < 256; i++) {
if (occur[i] > 0 && occur[i] < min) {
ch = (char) i;
min = occur[i];
}
}
return ch;
}
面试题 51:数组中的逆序对
/**
* 题目 51: 数组中的逆序对
* 在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数字组
* 成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
*
* 思路
* 如果遍历数组,对每个数字都和后面的数字比较大小,时间复杂度为O(n^2),效率太低。 *
* 利用归并排序的思想,先将数组分解成为n个长度为1的子数组,然后进行两两合并同时排好顺序。 *
* 在对两个子区域合并排序时,记左边区域(下标为start~mid)的指针为i,右边区域(下标为mid+1~end)的指针为j,
* 两个指针都指向该区域内最大的数字,排序时:
* (1)如果i指向的数字大于j指向的数字,说明:逆序对有j-mid个,我们把i指向的数字放入临时创建的排序数组中,然后令i-1,
* 指向该区域前一个数字,继续进行排序; *
* (2)如果i指向的数字小于等于j指向的数字,说明暂时不存在逆序对,将j指向的数字放入临时创建的排序数组中,然后令j-1,
* 指向该区域前一个数字,继续进行排序; *
* (3)某一子区域数字都放入排序数组后,将另一个子区域剩下的数字放入排序数组中,完成排序; *
* (4)最后将排序好的数字按顺序赋值给原始数组的两个子区域,以便合并后的区域与别的区域合并。
*/
public class E51 {
/**
*
* @param array 将要判断的数组
* @return 返回逆序对的个数
*/
public static int inversePairs(int [] array){
if(array==null||array.length<=0){
return 0;
}
return getCount(array,0,array.length-1);
}
/**
*
* @param array 将要判断的数组
* @param start 数组的开始
* @param end 数组的结束
* @return
*/
private static int getCount(int[] array, int start, int end) {// { 1, 2, 3, 4, 7, 6, 5 }
if(start>=end){
return 0;
}
//拆分 拆分成一个元素的
int mid=(end+start)>>1;
int left=getCount(array,start,mid);
int right=getCount(array,mid+1,end);
//合并
int count=0;//逆序对的个数
int i=mid;//左边区域的指针的最右侧
int j=end;//右边区域的指针的最右侧
int [] tmp=new int[end-start+1];//临时区域
int k=end-start;//临时区域的指针
while(i>=start && j>=mid+1){
if(array[i]>array[j]){//i指定的最大元素【最后一个】大于j指定的最大元素【最后一个】
count+=(j-mid);//逆序对个数是第二个数组的所有元素的个数
tmp[k--]=array[i--];//把较大的元素放到临时数组中,并且指针向前
}else{
tmp[k--]=array[j--];//把较大的元素放到临时数组中,并且指针向前
}
}
//
while(i>=start){//某一子区域都放入排序数组后 将另一个区域较小的剩下的数字放入排序数组中,完成排序
tmp[k--]=array[i--];
}
while(j>=mid+1){//某一子区域都放入排序数组后 将另一个区域较小的剩下的数字放入排序数组中,完成排序
tmp[k--]=array[j--];
}
//最后将排序好的数字按顺序赋值给原始数组的两个区域,以便合并后的区域与别的区域再进行合并,最后就是排序好的数组
for(k=0;k<tmp.length;k++){
array[k+start]=tmp[k];//tmp已经排序好的数组
}
return count+left+right;
}
}
面试题 52:两个链表的第一个公共节点
/**
* 题目 52:两个链表的第一个公共节点
* 输入两个链表,找出它们的第一个公共结点。
*
* 蛮力法:遍历第一个链表的结点,每到一个结点,就在第二个链表上遍历每个结点,判断是否相等。时间复杂度为O(m*n),效率低; *
* 使用栈:由于公共结点出现在尾部,所以用两个栈分别放入两个链表中的结点,从尾结点开始出栈比较。时间复杂度O(m+n),空间复杂度O(m+n)。 *
* 【利用长度关系:】计算两个链表的长度之差,长链表先走相差的步数,之后长短链表同时遍历,找到的第一个相同的结点就是第一个公共结点。 *
* 【利用两个指针:】一个指针顺序遍历list1和list2,另一个指针顺序遍历list2和list1,(这样两指针能够保证最终同时走到尾结点),
* 两个指针找到的第一个相同结点就是第一个公共结点。
*/
public class E52 {
public class ListNode{
int val;
ListNode next=null;
ListNode(int val){
this.val=val;
}
ListNode(){}
}
/**
* 方法一:使用栈
* @param pHead1
* @param pHead2
* @return 第一个公共结点
*/
public ListNode findFirstCommonNode(ListNode pHead1,ListNode pHead2){
Stack<ListNode> s1=new Stack<>();
Stack<ListNode> s2=new Stack<>();
if (pHead1==null || pHead2==null){
return null;
}
while(pHead1!=null){
s1.push(pHead1);
pHead1=pHead1.next;
}
while(pHead2!=null){//第二个链表
s2.push(pHead2);
pHead2=pHead2.next;
}
ListNode tmp=null;
while(!s1.empty()){
ListNode l1=s1.peek();
ListNode l2=s2.peek();
if (l1==l2){
tmp=s1.pop();
s2.pop();
}
}
return tmp;
}
/**
* 方法二:利用长度关系
* @param pHead1
* @param pHead2
* @return 第一个公共结点
*/
public ListNode findFirstCommonNode1(ListNode pHead1,ListNode pHead2){
int length1=getLength(pHead1);
int length2=getLength(pHead2);
int size=length1-length2;
ListNode longNode=pHead1;
ListNode shortNode=pHead2;
if (size<0){
longNode=pHead2;
shortNode=pHead1;
}
for (int i=0;i<size;i++){
longNode=longNode.next;
}
while (longNode!=null && longNode!=shortNode){
longNode=longNode.next;
shortNode=shortNode.next;
}
return longNode;
}
private int getLength(ListNode pHead){
int len=0;
while(pHead!=null){
len++;
pHead=pHead.next;
}
return len;
}
}
F.面试中各项能力
面试题 53:在排序数组中查找数字
面试题 53(1):数字在排序数组中出现的次数
/**
* 题目 53(1):数字在排序数组中出现的次数
* 统计一个数字在排序数组中出现的次数。例如输入排序数组{1, 2, 3, 3,
* 3, 3, 4, 5}和数字3,由于3在这个数组中出现了4次,因此输出4。
*/
public class F53 {
public int GetNumberOfK(int[] array, int k) {
int first = binarySearch(array, k);
int last = binarySearch(array, k + 1);
return last - first;
}
/**
*
* @param array 排序数组
* @param K 一个数字
* @return 返回该数字在数组中第一次出现的索引+1
* 如果数组中没有该数字,则返回最邻近且比该数字大的数字的索引+1
* 比数组中任意数字都大,返回数组长度
*/
private int binarySearch(int[] array, int K) {
int l = 0;
int h = array.length;
while (l < h) {
int m = l + (h - l) / 2;
if (array[m] >= K){
h = m; //取靠前的
}else{
l = m + 1;
}
}
return l;
}
}
面试题 53(2):0 ~ n-1 中缺失的数字
/**
* 题目 53(1):0 ~ n-1 中缺失的数字
* 一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字
* 都在范围0到n-1之内。在范围0到n-1的n个数字中有且只有一个数字不在该数组
* 中,请找出这个数字。
*
* * 分析易知,数组形式如下:
* * 如果从头到尾依次比较值与下标是否相等,时间复杂度为O(n),效率低。
* * 由于是排序数组,我们继续考虑使用二分查找算法:
* * 当中间数字等于其下标时,我们在后半部分查找;
* * 当中间数字不等于其下标时,
* * 1)如果中间数字的前一个数字也不等于其下标,则在前半部分查找;
* * 2)如果中间数字的前一个数字等于其下标,则说明中间数字的下标即为我们所要找的数字。
*/
public class F531 {
public int getMissingNumber(int [] arr){
if (arr==null || arr.length<=0){
return -1;
}
int low=0;
int high=arr.length-1;
while(low<=high){
int mid=(low+high)>>1;
if (arr[mid]!=mid){
if (mid==0 || arr[mid-1]==mid-1){
return mid;
}else {
high=mid+1;
}
}else {
low=mid+1;
}
}
return -1;
}
}
面试题 53(3):数组中数值和下标相等的元素
/**
* 题目 53(2):数组中数值和下标相等的元素
* 假设一个单调递增的数组里的每个元素都是整数并且是唯一的。请编程实
* 现一个函数找出数组中任意一个数值等于其下标的元素。例如,在数组{-3, -1,
* 1, 3, 5}中,数字3和它的下标相等。
*
* * 思路
* * 同53和53-1一样,不再从头到尾遍历,由于是排序数组,我们继续考虑使用二分查找算法:
* * 1)当中间数字等于其下标时,中间数字即为所求数字;
* * 2)当中间数字大于其下标时,在左半部分区域寻找;
* * 2)当中间数字小于其下标时,在右半部分区域寻找;
*/
public class F532 {
private int getNumberSameAsIndex(int [] arr){
if(arr==null||arr.length<=0){
return -1;
}
int low=0;
int high=arr.length-1;
while(low<=high){
int mid=(low+high)>>1;
if(arr[mid]>mid){//当中间数字大于其下标时,可能在左侧已经先出现了下标和元素相同的情况,重新定义再查找左侧的结束位置
high=mid-1;
}else if(arr[mid]<mid){//当中间数字小于其下标时,可能在右侧出现了下标和元素相同的情况,重新定义再查找右侧的结束位置
low=mid+1;
}else{
return mid;
}
}
return -1;
}
}
面试题 54:二叉树的第k大个节点
/**
* 题目 54: 二叉树的第k大个节点
* 给定一棵二叉搜索树,请找出其中的第k大的结点。
*
* 思路
* 考察对中序遍历的理解,中序遍历的结果是递增排序的
*/
public class F54 {
public class TreeNode{
int val=0;
TreeNode left=null;
TreeNode right=null;
public TreeNode(int val){
this.val=val;
}
public TreeNode(){
}
}
public TreeNode findKthNode(TreeNode root,int k){
if (root == null || k<=0) {
return null;
}
Stack<TreeNode> stack=new Stack<>();
int count=0;
while(root!=null || !stack.isEmpty()){
while(root!=null){
stack.push(root);
root=root.left;
}
if (!stack.isEmpty()){
root=stack.pop();
if (++count==k){
return root;
}
root=root.right;
}
}
return null;
}
}
面试题 55:二叉树的深度
/**
* 题目 55:二叉树的深度
* 输入一棵二叉树的根结点,求该树的深度。从根结点到叶结点依次经过的
* 结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
*
* 思路:
* 树的深度=max(左子树深度,右子树深度)+1,采用递归实现。
*/
public class F55 {
public class TreeNode{
int val=0;
TreeNode left=null;
TreeNode right=null;
public TreeNode(int val){
this.val=val;
}
public TreeNode(){
}
}
public int treeDepth(TreeNode root){
if (root==null){
return 0;
}
int left=treeDepth(root.left);
int right=treeDepth(root.right);
return left>right?(left+1):(right+1);
}
}
面试题 56:数组中数字出现的次数
面试题 56(1):数组中只出现一次的两个数
/**
* 数组中有两个出现一次的数字,其他数字都出现两次,找出这两个数字
*
* 思路:
* 可以用位运算实现,如果将所有所有数字相异或,则最后的结果肯定是那两个只出现一次的数字异或
* 的结果,所以根据异或的结果1所在的最低位,把数字分成两半,每一半里都还有只出现一次的数据和成对出现的数据
* 这样继续对每一半相异或则可以分别求出两个只出现一次的数字
* @param array
* @param num1
* @param num2
*/
public static void findNumsAppearOnce(int [] array,int num1[] , int num2[]) {
if(array == null || array.length <= 1){
num1[0] = num2[0] = 0;
return;
}
int len = array.length, index = 0, sum = 0;
for(int i = 0; i < len; i++){
sum ^= array[i];
}
for(index = 0; index < 32; index++){
if((sum & (1 << index)) != 0) {
break;
}
}
for(int i = 0; i < len; i++){
if((array[i] & (1 << index))!=0){
num2[0] ^= array[i];
}else{
num1[0] ^= array[i];
}
}
}
面试题 56(2):数组中唯一出现一次的数字
/**
* 数组a中只有一个数出现一次,其他数字都出现了3次,找出这个数字
* @param a
* @return
*/
public static int find1From3(int[] a){
int[] bits = new int[32];
int len = a.length;
for(int i = 0; i < len; i++){
for(int j = 0; j < 32; j++){
bits[j] = bits[j] + ( (a[i]>>>j) & 1);
}
}
int res = 0;
for(int i = 0; i < 32; i++){
if(bits[i] % 3 !=0){
res = res | (1 << i);
}
}
return res;
}
面试题 56 扩展
/**
* 数组a中只有一个数出现一次,其他数都出现了2次,找出这个数字
* @param a
* @return
*/
public static int find1From2(int[] a){
int len = a.length, res = 0;
for(int i = 0; i < len; i++){
res = res ^ a[i];
}
return res;
}
面试题 57:和为 s 的数字
面试题 57(1):和为 s 的两个数字
/**
* (一) 和为 s 的两个数字
* 输入一个递增排序的数组和一个数字 s,在数组中查找两个数,
* 使得他们的和正好是 s。如果有多对数字和等于 s,则输出任意一组即可。
*/
public ArrayList<Integer> FindNumberWithSum(int[] array,int sum){
ArrayList<Integer> list=new ArrayList<>();
int start=0;
int end=array.length-1;
while(start<end){
if (array[start]+array[end]>sum){
end--;
}else if (array[start]+array[end]<sum){
start++;
}else {
list=new ArrayList<Integer>();
list.add(array[start]);
list.add(array[end]);
break;
}
}
return list;
}
面试题 57(2): 和为 s 的连续正数序列
/**
* (二) 和为 s 的连续正数序列
* 输入一个正数 s,打印出所有和为 s 的连续正数序列(至少含有两个数)。
* 例如输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以结果打印出3个连续序列 1~5、4~6 和 7~8。
*/
public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
//存放结果
ArrayList<ArrayList<Integer> > result = new ArrayList<>();
//两个起点,相当于动态窗口的两边,根据其窗口内的值的和来确定窗口的位置和大小
int plow = 1,phigh = 2;
while(phigh > plow){
//由于是连续的,差为1的一个序列,那么求和公式是(a0+an)*n/2
int cur = (plow + phigh) * (phigh - plow + 1) / 2;
//相等,那么就将窗口范围的所有数添加进结果集
if(cur == sum){
ArrayList<Integer> list = new ArrayList<>();
for(int i=plow;i<=phigh;i++){
list.add(i);
}
result.add(list);
plow++;
}else if(cur < sum){
//如果当前窗口内的值之和小于sum,那么右边窗口右移一下
phigh++;
}else{
//如果当前窗口内的值之和大于sum,那么左边窗口右移一下
plow++;
}
}
return result;
}
面试题 58:翻转字符串
面试题 58(1): 翻转单词顺序
/**
* (一) 翻转单词顺序
* 输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。
* 为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. ",
* 则输出"student. a am I"。
*/
//首先想到split()方法,但这要开辟新的数组,占内存空间
public String reverseSentence(String str) {
if(str.length()==0 || str.trim().equals("")){
return str;
}
String[] str1 = str.split(" ");
StringBuffer sb = new StringBuffer();
for(int i=str1.length-1;i>=0;i--){
if(i==0){
sb.append(str1[i]);
}else{
sb.append(str1[i]+" ");
}
}
return sb.toString();
}
//首先实现翻转整个句子:只需要在首尾两端各放置一个指针,交换指针所指的数字,
//两端指针往中间移动即可。之后根据空格的位置,对每个单词使用同样的方法翻转即可。
public String reverseSentence2(String str) {
if (str == null || "".equals(str)){
return "";
}
char[] targetArr = str.toCharArray();
int begin = 0; //表示字符串的前一个指针
int end = targetArr.length-1; //表示字符串的后一个指针
char[] reverseArr = reverse(targetArr, begin, end);
end = 0;
begin = 0;
while(end < targetArr.length){
if (reverseArr[end] == ' '){
//当后面一个指针遇到空字符串时
reverse(targetArr,begin,--end);
begin = ++end;
end = ++begin;
}else if (end== targetArr.length -1){
reverse(targetArr,begin,end);
begin = ++end;
end = ++begin;
} else if(reverseArr[begin] == ' '){
//当字符串中第一个就是空字符串的时候
++begin;
++end;
}else {
++end;
}
}
return String.valueOf(reverseArr);
}
private char[] reverse(char[] arr, int begin, int end) {
while (begin < end) {
char temp = arr[begin];
arr[begin] = arr[end];
arr[end] = temp;
++begin;
--end;
}
return arr;
}
面试题 58(2): 左旋转字符串
/**
* (二) 左旋转字符串
* 字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。
* 请定义一个函数实现字符串左旋转操作的功能。比如输入字符串"abcdefg"和数
* 字2,该函数将返回左旋转2位得到的结果"cdefgab"。
*
* 思路:本题思路和上一道题翻转单词顺序的原理一模一样,
* 只是上一道题有空格,这道题没空格,其实这道题还更简单。
* 先分别翻转前半部分字符串和后半部分字符串,最后翻转整个字符串即可。
*/
/**
*
* @param chars 哪个字符串进行翻转
* @param num 从哪里开始
* @return 翻转后新的字符串
*/
public String leftRotateString(String chars ,int num){
if(chars==null||chars.length()<=0){
return chars;
}
//以num为截止的前段翻转
char [] tmp=chars.toCharArray();
tmp=reverseSb(chars,0,num-1);
//以num为开始的后段翻转
tmp=reverseSb(String.valueOf(tmp), num,chars.length()-1);
//整体语句翻转
tmp=reverseSb(String.valueOf(tmp), 0,chars.length()-1);
return String.valueOf(tmp);
}
/**
*
* @param chars 要翻转的字符串
* @param start 从哪里开始翻转
* @param end 到哪里截止翻转
* @return 新的翻转后的字符串
*/
private char [] reverseSb(String chars, int start, int end) {
char[] ct=chars.toCharArray();
while(start<end){
char tmp=ct[start];
ct[start]=ct[end];
ct[end]=tmp;
start++;
end--;
}
return ct;
}
//使用substring的简洁算法
public String leftRotateString2(String str,int n) {
if(str.equals("")){
return str;
}
if (n>str.length() || n<0){
return null;
}
return str.substring(n,str.length()) + str.substring(0,n);
}
面试题 59:队列的最大值
面试题 59(1): 滑动窗口最大值
/**
* (一) 滑动窗口最大值
* 给定一个数组和滑动窗口的大小,请找出所有滑动窗口里的最大值。例如,
* 如果输入数组{2, 3, 4, 2, 6, 2, 5, 1}及滑动窗口的大小3,那么一共存在6个
* 滑动窗口,它们的最大值分别为{4, 4, 6, 6, 6, 5},
*/
/**
*
* @param num 某个判断的数组
* @param size 滑动窗口大小
* @return 返回每个滑动窗口的最大元素数组
*/
public int[] maxInWindows(int [] num,int size){
if(num==null||num.length<=0||size<=0||size>num.length){
return null;
}
int [] max=new int[num.length-(size-1)];//声明每个滑动窗口最大元素数组
ArrayDeque<Integer> indexDeque=new ArrayDeque<>();//存储暂时大元素的索引
for(int i=0;i<size-1;i++){
while(!indexDeque.isEmpty()&&num[i]>num[indexDeque.getFirst()]){
indexDeque.removeLast();//把较小的干掉
}
//存储较大的值
indexDeque.addLast(i);//加入较大的此数在数组的索引
}
//处理每个滑动窗口的边缘元素
int j=0;
for(int i=size-1;i<num.length;i++){//从最后一个滑动窗口的元素开始,前两个已经比较了,大的已经进入了队列,后一个并没有比较,
while(!indexDeque.isEmpty()&&num[i]>num[indexDeque.getLast()]){
indexDeque.removeLast();//把队列的所有元素和数组的元素进行比较,6>4 有元素已经大于队列的元素了
}
//没有判断滑动窗口的大小
if(!indexDeque.isEmpty()&&(i-indexDeque.getFirst())>=size){
indexDeque.removeFirst();//头部元素,这个是曾经的最大元素,移除它
}
indexDeque.addLast(i);
max[j++]=num[indexDeque.getFirst()];//通过弹出该索引,放到最大元素数组中。
}
return max;
}
面试题 59(2): 队列的最大值
/**
* (二) 队列的最大值
* 请定义一个队列并实现函数max得到队列里的最大值,要求函数max、
* push_back和pop_front的时间复杂度都是O(1)。
*
* 一个dataQueue正常入列、出列元素,为了以O(1)的时间获取当前队列的最大值,
* 需要使用一个maxQueue存放当前队列中最大值。具体来说就是,
* 如果即将要存入的元素比当前最大值还大,那么存入这个元素;否则再次存入当前最大值。
*/
private Deque<Integer> maxDeque=new LinkedList<>();//最大队列
private Deque<Integer> dataDeque=new LinkedList<>();//数据队列
public void push_back(int number){
dataDeque.offerLast(number);
while(!maxDeque.isEmpty()&&maxDeque.getLast()<number){
maxDeque.removeLast();
}
maxDeque.addLast(number);
}
public void pop_front(){
if(dataDeque.isEmpty()){
throw new RuntimeException("队列已空");
}
int i=dataDeque.pollFirst();
if(i==maxDeque.getFirst()){
maxDeque.removeFirst();
}
}
public int max(){
if(maxDeque==null){
System.out.println("队列为空,无法删除");
return 0;
}
return maxDeque.getFirst();
}
面试题 60:n个骰子的点数
/**
* 题目 60: n个骰子的点数
* 把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
* n个骰子,我们可以投掷n次,累加每次掷出的点数即可。
*/
public class F60 {
private int sideNum=6;//骰子的面数
//把出现的n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值概率。
public void printProbability(int n){
if(n<1){
return;
}
int maxVal=n * sideNum;//计算出n个筛子的点数的和是多少
//声明的是用一个大小为6n-n+1的数组存放每个可能的点数和出现的频率
int [] probabilities=new int[maxVal-n+1];//比如:该数组的下标存放的是点数和 值是当前的点数出现的次数n
getPro(n,n,0,probabilities);
int total=(int)Math.pow(sideNum,n);//总的点数和的情况
for(int i=n;i<=maxVal;i++){//最小的肯定是n个点,最大的肯定是maxVal个点
//所有骰子朝上一面的点数和为s
System.out.println(n+"个骰子,朝上一面的点数之和"+"s是"+i+"个点的频率是:"+probabilities[i-n]+"/"+total);
}
}
/**
*
* @param original 筛子的总个数
* @param cur 当前的筛子
* @param sum 每个骰子的面加起来的总点数
* @param p 可能的点数和出现的频率
*/
private void getPro(int original , int cur,int sum,int [] p){
if(cur==0){
p[sum-original]++;
return;
}
for(int i=1;i<=sideNum;i++){
getPro(original,cur-1,sum+i,p);
}
}
}
面试题 61:扑克牌中的顺子
/**
* 题目 61: 扑克牌中的顺子
* 从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。
* 2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王可以看成任意数字。
*
* * 输入为大小等于5的数组(大小王记为0),输出为布尔值。具体步骤如下:
* * 1)进行对5张牌进行排序;
* * 2)找出为0的个数;
* * 3)算出相邻数字的空缺总数;
* * 4)如果0的个数大于等于空缺总数,说明连续,反之不连续;
* * 5)记得判断相邻数字是否相等,如果有出现相等,说明不是顺子。
* * 正常的顺子比如23456,假如含有大小王呢?03467也能构成顺子,虽然4和6中间有一个间隔,但是因为存在一张大小王,
* * 刚好可以让它成为5而填补上4和6之间的间隔。再假设是00367也能构成顺子,3和6之间有两个间隔但是刚好有两个王可以填补。
* * 更特殊的02345, 00234, 这种没有本身间隔的牌组合,因为大小王可以作任何数字,显然也构成顺子。
* * 接下来看不能构成顺子的例子,34578,以及03678或者00378,可以发现当王的个数比间隔数少的话就不能构成顺子,
* * 反之如果王的个数大于等于间隔数,就能构成顺子。
* * 要计算相邻两张牌的间隔,需要牌组合已经有序,因此第一步就要对五张牌排序。因此这种思路需要O(nlgn)的时间。
*/
public class F61 {
public boolean isCount(int[] numbers){
if(numbers==null||numbers.length<=0){
return false;
}
Arrays.sort(numbers);
int jokerCount=0;
for (int i=0;i<4;i++){
if (numbers[i]==0){
jokerCount++;
}
}
int gap=0;
for (int i=numbers.length-1;i>jokerCount;i--){
if (numbers[i]==numbers[i-1]){
return false;
}
gap+=numbers[i]-numbers[i-1]-1;
}
return gap<=jokerCount;
}
}
面试题 62:圆圈中最后剩下的数字
/**
* 题目 62:圆圈中最后剩下的数字
* 0, 1, …, n-1 这 n个数字排成一个圆圈,从数字 0 开始每次从这个圆圈里
* 删除第 m 个数字。求出这个圆圈里剩下的最后一个数字。
*/
public class F62 {
public int last(int n,int m){
if (n<0 || m<=0){
return -1;
}
List<Integer> list=new LinkedList<>();
for (int i=0;i<n;i++){
list.add(i);
}
int p=0;
while (list.size()>1){
for (int k=0;k<m-1;k++){
p++;
if (p==list.size()){
p=0;
}
}
list.remove(p);
if (p==list.size()){
p=0;
}
}
return list.get(0);
}
/**
* 解法2:数学公式推导
* f(n, m) = [f(n-1, m) + m] % n ,n > 1
* 且恒有f(1, m) = 0, n = 1,因为如果只有一个数(这个数是0),那么无需删除,最后一个剩下的数就是它。
* 既然知道了f(1, m)根据上式就能求出f(2, m),以此类推,只需一个循环就能求出f(n, m)
*/
public int last1(int n,int m){
if (n<=0 || m<=0){
return -1;
}
int f=0;
for (int i=2;i<=n;i++){
f=(f+m)%i;
}
return f;
}
}
面试题 63:股票的最大利润
/**
* 题目 63: 股票的最大利润
*/
public class F63 {
public int getMaxDiff(int[] prices){
if (prices==null || prices.length<2){
return 0;
}
int min=prices[0];
int maxDiff=0;
int curDiff=0;
for (int i=1;i<prices.length;i++){
if (prices[i]<min){
min=prices[i];
}
curDiff=prices[i]-min;
if (curDiff>maxDiff){
maxDiff=curDiff;
}
}
return maxDiff;
}
}
面试题 64:求 1 + 2 + … + n
/**
* 题目 64 :求 1 + 2 + ... + n
* 要求不能使用乘除法、for、while、if、else、switch、case
*
*
*/
public class F64 {
//1.需利用逻辑与的短路特性实现递归终止。
//2.当n==1时,(n>1)&&((sum+=Sum_Solution(n-1))>0)只执行前面的判断,为false,然后直接返回0;
//3.当n>1时,执行sum+=Sum_Solution(n-1),实现递归计算Sum_Solution(n)。
public int getSum(int n){
int sum=n;
boolean flag=(n>1) && ( (sum+=getSum(n-1)) > 0);
// if(n>1){
// sum+=getSum(n-1)
// }
return sum;
}
public int getSum2(int n){
// (n(n+1))/2 == (n^2+n)>>1
return ( (int)Math.pow(n,2) + n )>>1;
}
}
面试题 65:不用加减乘除做加法
/**
* 题目 65: 不用加减乘除做加法
*
*/
public class F65 {
//首先看十进制是如何做的: 5+7=12,三步走
//第一步:相加各位的值,不算进位,得到2。
//第二步:计算进位值,得到10. 如果这一步的进位值为0,那么第一步得到的值就是最终结果。
//
//第三步:重复上述两步,只是相加的值变成上述两步的得到的结果2和10,得到12。
//
//同样我们可以用三步走的方式计算二进制值相加: 5-101,7-111 第一步:相加各位的值,不算进位,得到010,二进制每位相加就相当于各位做异或操作,101^111。
//
//第二步:计算进位值,得到1010,相当于各位做与操作得到101,再向左移一位得到1010,(101&111)<<1。
//
//第三步重复上述两步, 各位相加 010^1010=1000,进位值为100=(010&1010)<<1。
// 继续重复上述两步:1000^100 = 1100,进位值为0,跳出循环,1100为最终结果。
public int add(int num1,int num2){
while(num2!=0){
int sum=num1^num2;
int carrry=(num1&num2)<<1;
num1=sum;
num2=carrry;
}
return num1;
}
public int add2(int num1,int num2){
BigDecimal n1=new BigDecimal(num1);
BigDecimal n2=new BigDecimal(num2);
return n1.add(n2).intValue();
}
}
面试题 66:构建乘积数组
/**
* 题目 66: 构建乘积数组
*
*/
public class F66{
public int[] multiply(int[] A) {
int length = A.length;
int[] B = new int[length];
if(length != 0 ){
B[0] = 1;
//计算下三角连乘
for(int i = 1; i < length; i++){
B[i] = B[i-1] * A[i-1];
}
int temp = 1;
//计算上三角
for(int j = length-2; j >= 0; j--){
temp *= A[j+1];
B[j] *= temp;
}
}
return B;
}
G.两个面试案例
面试题 67:把字符串转换成整数
/**
* 题目 67: 把字符串转换成整数
* 将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。
* 数值为0或者字符串不是一个合法的数值则返回0
*/
public class G67 {
private boolean valid=false;
public int strToInt(String str){
if (str==null || str.length()==0){
return 0;
}
boolean isNegative=false;
long number=0;
for (int i=0;i<str.length();i++){
if ( (i==0 && str.charAt(i)=='+') || (i==0&&str.charAt(0)=='-') ){
if (str.charAt(i)=='-'){
isNegative=true;
}
if (str.length()==1){
return 0;
}
continue;
}
if (str.charAt(i)<'0' || str.charAt(i)>'9'){
return 0;
}
int flag=isNegative?-1:1;
number=number*10+flag*(str.charAt(i)-'0');
if (!isNegative && number>Integer.MAX_VALUE || isNegative && number<Integer.MIN_VALUE){
return 0;
}
}
valid=true;
return (int) number;
}
}
面试题 68:树中的两个节点的最低公祖先
/**
* 题目 68; 树中的两个节点的最低公祖先
*/
public class G68 {
/**
* (一) 如果这颗树是二叉搜索树
*/
private static class TreeNode{
private TreeNode left,right;
private int value;
}
public TreeNode find(TreeNode node1,TreeNode node2 ,TreeNode root){
int value=root.value;
//如果当前节点比两个输入都大,那么最低公共祖先一定在当前结点的左子树中,
//相当于这个结点比根大,比另个结点也大 根一定在其左子树上
if(value> node1.value&&value>node2.value){
return find(node1,node2,root.left);
}
//如果当前节点比两个输入都小,那么最低公共祖先一定在当前结点的右子树中,
//相当于这个结点比根小,比另个结点也小 根一定在其右子树上
if(value< node1.value&&value<node2.value){
return find(node1,node2,root.right);
}
//否则就是根节点
return root;
}
/**
* (二) 如果这棵树只是一颗普通的树,但是它拥有指向父结点的指针,
* 该如何求最低公共祖先?
*
* 思路:转换为两个链表求第一个公共节点问题
*/
private static class TreeNode1{
TreeNode1 parent;//指向父节点的指针
private int value;
public TreeNode1(int value){
this.value=value;
}
}
public TreeNode1 find1(TreeNode1 node1,TreeNode1 node2){
if (node1==null || node2==null){
return null;
}
TreeNode1 cur1=node1;
TreeNode1 cur2=node2;
int len1=getLength(cur1);
int len2=getLength(cur2);
if (len1>len2){
node1=moveIndex(node1,len1,len2);
}
if (len2>len1){
node2=moveIndex(node2,len2,len1);
}
while (node1!=null&&node2!=null){
if(node1==node2){
return node1;
}
node1=node1.parent;
node2=node2.parent;
}
return null;
}
private int getLength(TreeNode1 node){
int length=0;
while (node!=null){
node=node.parent;
length++;
}
return length;
}
private TreeNode1 moveIndex(TreeNode1 node,int longLen,int shortLen){
for (int i = 0; i <longLen-shortLen ; i++) {
node=node.parent;
}
return node;
}
}
本文精选《剑指Offer》中的经典面试题,通过Java代码实现单例模式、数组操作、链表处理、二叉树构建及遍历、字符串处理等核心算法与数据结构题目,旨在提升编程能力和面试技巧。

5289

被折叠的 条评论
为什么被折叠?



