剑指offer学习2(Java语言版)

面试题2:单例模式

设计一个类,我们只能生成该类的一个实例

这道题考察的是单例模式,这里给个对单例模式讲解的比较详细的网址 菜鸟教程|单例模式

面试题3:数组中重复的数字

在这里插入图片描述
分析:利用数组下标是连续的,且在该题中与数组存储的元素具有对应关系,如果没有重复,则排序后刚好和下标对应;如果有重复,则排序后存在不能和下标对应的情况

public class Demo03_1 {
	/**
	 * @param arr 输入的数组
	 * @return 返回 -1表示要么数字超范围,要么没有重复的数字
	 */
	public static int search(int[] arr) {
		if (arr == null) {
			return -1;
		}
		for (int i = 0; i < arr.length; i++) {
			while (arr[i] != i) {
				int index = arr[i];
				// 验证数字
				if (index < 0 || index > arr.length - 1) {
					System.out.print("数组元素超出 [0,n-1]范围 ");
					return -1;
				}
				// 查重
				if (arr[i] == arr[index]) {
					return arr[i];
				}
				// 交换
				int temp = arr[index];
				arr[index] = arr[i];
				arr[i] = temp;
			}
		}
		return -1;
	}

	public static void main(String[] args) {
		// 测试用例
		int[] arr0 = null; // 空指针 
		int[] arr1 = {}; // 测空数组
		int[] arr2 = { 2, 3, 1, 0, 2, 5, 3 }; // 有重复数字:数字在 0 ~ n-1
		int[] arr3 = { 2, 3, 7, 0, 3, 5, 3 }; // 数字超出范围:数字不在 0 ~ n-1
		int[] arr4 = { 2, 0, 1, 3, 5, 4 }; // 没有重复数字

		// 测试结果
		System.out.println("arr0: " + search(arr0));
		System.out.println("arr1: " + search(arr1));
		System.out.println("arr2: " + search(arr2));
		System.out.println("arr3: " + search(arr3));
		System.out.println("arr4: " + search(arr4));
	}
}

测试输出:

arr0: -1
arr1: -1
arr2: 2
数组元素超出 [0,n-1]范围 arr3: -1
arr4: -1

在这里插入图片描述
分析:这里只是在题目一的基础上加1,使下标对应比所存的数字小一,所以可以在题目一的解答上稍作修改。

public class Demo03_2 {
	/**
	 * @param arr 输入的数组
	 * @return 返回 -1表示要么数字超范围,要么没有重复的数字
	 */
	public static int search(int[] arr) {
		if (arr == null) {
			return -1;
		}
		for (int i = 0; i < arr.length; i++) {
			while (arr[i] != i + 1) {
				int index = arr[i] - 1;
				// 验证数字
				if (index < 0 || index > arr.length - 1) {
					System.out.print("数组元素超出 [1,n]范围 ");
					return -1;
				}
				// 查重
				if (arr[i] == arr[index]) {
					return arr[i];
				}
				// 交换
				int temp = arr[index];
				arr[index] = arr[i];
				arr[i] = temp;
			}
		}
		return -1;
	}

	public static void main(String[] args) {
		// 测试用例
		int[] arr0 = null; // 空指针 
		int[] arr1 = {}; // 测空数组
		int[] arr2 = { 2, 3, 5, 4, 3, 2, 6, 7 }; // 有重复数字:数字在 1 ~ n
		int[] arr3 = { 2, 9, 5, 4, 3, 2, 6, 7 }; // 数字超出范围:数字不在 1 ~ n
		int[] arr4 = { 2, -1, 5, 4, 3, 2, 6, 7 }; // 数字超出范围:数字不在 1 ~ n
		int[] arr5 = { 2, 1, 3, 8, 4, 6, 5, 7 }; // 没有重复数字

		// 测试
		System.out.println("arr0: " + search(arr0));
		System.out.println("arr1: " + search(arr1));
		System.out.println("arr2: " + search(arr2));
		System.out.println("arr3: " + search(arr3));
		System.out.println("arr4: " + search(arr4));
		System.out.println("arr5: " + search(arr5));
	}
}

测试输出:

arr0: -1
arr1: -1
arr2: 3
数组元素超出 [1,n]范围 arr3: -1
数组元素超出 [1,n]范围 arr4: -1
arr5: -1

面试题4:二维数组中的查找

在这里插入图片描述
分析:首先处理输入为空数组的情况,还应该考虑是否存在行向量和列向量的情况。观察可以发现题目描述的矩阵可发现:目标值target与某一值arr[i][j]比较的时候,如果target = arr[i][j]直接返回true;如果target < arr[i][j],则应该在该值左或上方找,i++;如果target > arr[i][j],则应该在该值的右或下方找。所以我们应该开始从最左下角找,这样就只需要向上和向右走了,直到找到这个值返回true或者搜索完这个矩阵没找到返回false。

public class Demo04 {
	public static void main(String[] args) {
		// 测试用例
		int[][] m0 = null; // 空指针
		int[][] m1 = {}; // 空数组
		int[][] m2 = { { 1 }, { 3 }, { 6 }, { 23 }, { 49 }, { 60 } }; // 列向量
		int[][] m3 = { { 1, 3, 6, 23, 49, 60 } }; // 行向量
		int[][] m4 = { // 正常
						{ 1, 2, 8, 9 }, 
						{ 2, 4, 9, 12 }, 
						{ 4, 7, 10, 13 }, 
						{ 6, 8, 11, 15 } 
					 };
		// 二维数组不符合递增情况,不考虑
		// 二维数组行向量不等长,不考虑

		int[] target = { 3, -2, 6, 4, 9 };

		// 测试
		System.out.println("m0, target = 3: " + search(m0, target[0])); // false
		System.out.println("m1, target = -2: " + search(m1, target[1]));// false
		System.out.println("m2, target = 6: " + search(m2, target[2])); // true
		System.out.println("m3, target = 4: " + search(m3, target[3])); // false
		System.out.println("m4, target = 9: " + search(m4, target[4])); // true
	}

	public static boolean search(int[][] m, int target) {
		// 判空
		if (m == null || m.length == 0) {
			return false;
		}
		int row = m.length;
		int col = m[0].length;
		int i = row - 1, j = 0;
		while (i >= 0 && j <= col -1) {
			if (m[i][j] > target) {
				i--;
			} else if (m[i][j] < target) {
				j++;
			} else {
				return true;
			}
		}
		return false;
	}

}

测试输出:

m0, target = 3: false
m1, target = -2: false
m2, target = 6: true
m3, target = 4: false
m4, target = 9: true

面试题5:替换空格

在这里插入图片描述

分析:这个题不太适合Java,因为可以用替换函数s.replace(" ", “%20”);直接返回,暂时跳过。

面试题6:从尾到头打印链表

在这里插入图片描述
分析:LeetCode上算法题,头结点都是带数据的,所以这里我们也默认头结点带数据。
首先,需要考虑到链表为null和长度为1的特殊情况,打印一般是要求返回一个存储结点值的List。

  • 方法1:遍历一遍存储在栈中,然后依次弹出来,有循环和递归两种;
  • 方法2:将值存储到一个ArrayList反向遍历;

用递归方法代码简洁,但是当列表很长,调用方法栈帧很多层,很吃内存,所以还是用循环的方式要好一点。

public class Demo06 {
	public static void main(String[] args) {
		// 测试用例
		// 一个测试用例 list = [9,8,7,6,5,4,3,2,1]
		ListNode root = new ListNode(9);
		ListNode node = root;
		for (int i = 8; i > 0; i--) {
			node.next = new ListNode(i);
			node = node.next;
		}

		// 测试
		printLinkedList(null); // null
		printLinkedList(root); // [9,8,7,6,5,4,3,2,1]
	}

	// 用了额外的空间,空间复杂度为O(n)
	private static void printLinkedList(ListNode root) {
		// 底层是数组, 所以随机访问get(i)复杂度是O(1)
		ArrayList<Integer> list = new ArrayList<Integer>(); 
		if (root == null) {
			return;
		}
		// 遍历
		while (root != null) {
			list.add(root.val);
			root = root.next;
		}
		// 反向打印
		for (int i = list.size() - 1; i >= 0; i--) {
			System.out.print(list.get(i) + " ");
		}
		list.clear();
	}
}

class ListNode {
	int val;
	ListNode next;

	ListNode(int x) {
		val = x;
	}
}

测试输出:

1 2 3 4 5 6 7 8 9 

面试题7:重建二叉树

在这里插入图片描述
分析:二叉树是有序树,前序遍历的第一个值即为根节点,根据根节点的值我们可以在中序遍历中找到根节点的位置j,j前面部分位于根节点左边,后面的部分位于根节点右边,这样我们就又可以在先序遍历序列中找到根节点的左右子节点,依次递归下去。
同时我们还要注意给出的序列中不能有重复的值,不然是不能找到根节点的。

// 定义二叉树节点
class TreeNode {
	int val;
	TreeNode left;
	TreeNode right;

	TreeNode(int x) {
		val = x;
	}
}

public class Demo07 {
	public static void main(String[] args) {
		// 测试序列
		int[] preOrder = { 1, 2, 4, 7, 3, 5, 6, 8 };
		int[] inOrder = { 4, 7, 2, 1, 5, 3, 8, 6 };
		// 重建
		TreeNode root = reConstructBinaryTree(preOrder, inOrder);
		// 测试先序遍历
		traversePre(root);
		// 测试中序遍历
		System.out.println("\n---------------");
		traverseIn(root);
	}

	public static TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        TreeNode root = reConstructBinaryTree(pre,0,pre.length-1,in,0,in.length-1);
        return root;
    }

	// 根据先序遍历和中序遍历创建二叉树
	private static TreeNode reConstructBinaryTree(int[] pre, int startPre,int endPre,int[] in, int startIn,int endIn){
		// 递归结束条件
        if(startPre>endPre||startIn>endIn)
            return null;
        // 新建一个节点
        TreeNode root = new TreeNode(pre[startPre]);
        
        for(int i = startIn; i <= endIn; i++)
            if(in[i] == pre[startPre]){// 在中序遍历中找到根节点位置
            	/**
            	 * 中序遍历中根节点在i位置,先序遍历中根节点在startPre位置,则:
            	 * 先序序列左子树范围:[startPre+1,startPre+i-startIn]
            	 * 中序序列左子树范围:[startIn,i-1]
            	 * 先序序列右子树范围:[i-startIn+startPre+1,endPre]
            	 * 中序序列右子树范围:[i+1,endIn]
            	 */
                root.left = reConstructBinaryTree(pre,startPre+1,startPre+i-startIn,in,startIn,i-1);
                root.right = reConstructBinaryTree(pre,i-startIn+startPre+1,endPre,in,i+1,endIn);
                break;
            }
        return root;
    }
	
	// 中序遍历
	public static void traverseIn(TreeNode root) {
		if (root != null) {
			traverseIn(root.left);
			System.out.print(root.val + " ");
			traverseIn(root.right); 
		}
	}

	// 先序遍历
	public static void traversePre(TreeNode root) {
		if (root != null) {
			System.out.print(root.val + " ");
			traversePre(root.left);
			traversePre(root.right); 
		}
	}

}

面试题8:二叉树的下一个结点

在这里插入图片描述

面试题9:用两个栈实现队列

在这里插入图片描述
分析:可以用stack1作为数据存储,添加的时候先已有的数据全压入stack2,然后在stack1添加新的元素,最后把stack2的元素压回来;如果是删除,直接从stack1栈顶pop一个元素即可,代价很小。这种方法缺点是入栈需要执行2n出栈操作和2n+1次入栈操作。

public class Quene<T> {
	private Stack<T> stack1 = new Stack<T>();
	private Stack<T> stack2 = new Stack<T>();

	public void appendTail(T t) {
		while (!stack1.isEmpty()) {
			stack2.push(stack1.pop());
		}
		stack1.push(t);
		while (!stack2.isEmpty()) {
			stack1.push(stack2.pop());
		}
	}

	public T deleteHead() {
		if (!stack1.isEmpty()) {
			return stack1.pop();
		}
		return null;
	}
}
public class Demo09 {
	public static void main(String[] args) {
		Quene<Integer> quene = new Quene<Integer>();
		// 入队测试
		for (int i = 0; i < 10; i++) {
			quene.appendTail(i);
		}
		// 出队测试
		Integer temp = null;
		while ((temp = quene.deleteHead()) != null) {
			System.out.print(temp + ", ");
		}
		// 空队出队
		System.out.println();
		System.out.println(quene.deleteHead());
	}
}

测试输出:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 
null

思考题:用两个队列实现一个栈?
分析:其实用一个队列就可以实现一个栈,入栈的时候直接入队列,出栈的时候现将队列前面n-1个依次出队列入队列,然后将第n个出队列取出,第n个取出不在入队。

面试题10:斐波那契数列

在这里插入图片描述
分析:该题既可以用循环实现,也可以用递归实现。递归占用的内存相对较大,所以我们还是考虑用循环实现。

public class Demo10 {
	// 0 1 1 2 3 5 8 13
	public static void main(String[] args) throws Exception {
		// 测试
		for (int n = 0; n < 10; n++) {
			System.out.println("fibonacci(" + n + ") = " + fibonacci(n));
		}
		System.out.println("fibonacci(-1) = " + fibonacci(-1)); // 该行会抛出异常
	}

	private static int fibonacci(int n) throws Exception {
		int[] f = { 0, 1 };
		int fn = f[0] + f[1];
		if (n >= 2) {
			while (n-- > 1) {
				fn = f[0] + f[1];
				f[0] = f[1];
				f[1] = fn;
			}
		} else if (n >= 0) {
			return f[n];
		} else {
			throw new Exception("n < 1,输入不和法!!");
		}

		return fn;
	}
}

测试输出:

fibonacci(0) = 0
fibonacci(1) = 1
fibonacci(2) = 1
fibonacci(3) = 2
fibonacci(4) = 3
fibonacci(5) = 5
fibonacci(6) = 8
fibonacci(7) = 13
fibonacci(8) = 21
fibonacci(9) = 34
Exception in thread "main" java.lang.Exception: n < 1,输入不和法!!
	at offer.ex10.Demo10.fibonacci(Demo10.java:34)
	at offer.ex10.Demo10.main(Demo10.java:16)

在这里插入图片描述
分析:
当n=1时,有一种跳法;
当n=2时,有两种跳法;
当n>2时,记所有跳法为f(n),先跳1次,则可以跳1级或2级,所以有f(n) = f(n-1) + f(n-2);
该问题可以转化成题目一中的斐波拉契数列,代码略。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值