《剑指offer》最全Java版(1-20题)(长期更新)

前言

《剑指offer》总结了面试中许多常见的算法题,对于提高编程和算法很有帮助。但是原书是用c语言实现的,我参考原书并加上自己的见解整理了Java版本。为了精简内容,很多细节未能呈现,更完整详细的版本请见我的Github。本人代码小白,正在砥砺前行,考虑如有欠佳,欢迎交流指正。

02_实现Singleton模式

1、懒汉式(单线程版)[不可用]
描述:这种方式是最基本的实现方式。这种实现最大的问题就是不支持多线程。

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
        if (instance == null)
            instance = new Singleton();    
        return instance;  
    }  
}

2、懒汉式(多线程版)[不推荐使用]
描述:每次执行getInstance()方法都要加同步锁。而加同步锁是一件非常消耗时间和性能的工作,严重影响效率。我们只有在没有实例化对象时才需要加锁,否则直接返回对象即可。在下一个单例模式实现方法中我们将对此进行优化。

public class Singleton { 
    private static Singleton instance;
    private Singlenton(){}
    
    public static Singleton getInstance(){
        synchronized(Singleton.class){
            if(instance == null)
                instance = new Singlenton();
        }
        return instance;
    }
}

3、双检锁/双重校验锁(DCL,即 double-checked locking)[推荐使用]

public class Singleton { 
//    必须使用volatile,new一个新对象是多个过程,必须保证该过程不发生重排序。
    private volatile static Singleton instance;
    private Singlenton(){}
    
    public static Singleton getInstance(){
        if( instance == null){
            synchronized(Singleton.class){
                if(instance == null)
                    instance = new Singlenton();
            }
        }
        return instance;
    }
}

4、 饿汉式[可使用]
描述:利用类加载机制。缺点是类加载后就一直存在,如果一直未使用,则会浪费内存。

public class Singleton{
    private final static Singleton instance = new Singleton();
    private Singleton() = {}; 
    
    public static Singleton getInstance(){
        return instance;
    }
}

5、静态内部类法[推荐使用]
描述:利用的依然是类加载的机制。并且静态内部类避免了静态实例在Singleton类加载的时候就创建对象,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。

public class Singleton{
    priavte Singleton(){}

    private static class SingletonInstance{
        praivate static final Singleton instance = new Singleton(); 
    }
    public static Singleton getInstance(){
        return SingletonInstance.instance;
    }
}

6、枚举法[可使用]
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {}
}

03_01找出数组中重复的数字

在一个长度为n的数组里的所有数字都在0到n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了, 也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组{2, 3, 1, 0, 2, 5, 3}, 那么对应的输出是重复的数字2或者3。

1、排序法
分析:最容易想到的就是先对数组进行排序,再对数组进行一遍遍历,比较相邻的元素是否相等。时间复杂度为O(nlogn)。

	public boolean duplicate(int[] arr) {
        if (arr == null || arr.length == 0)
            return false;
        Arrays.sort(arr);
        for (int i = 0; i < arr.length - 1; i++) {
            if (arr[i] == arr[i + 1])
                return true;
        }
        return false;
    }

2、利用Set集合
分析:因为要判断每个元素是否只有一个,使用Set集合来判断也是不错的选择。只要遍历数组每个元素,如果Set中已经有该元素,说明有重复,直接返回true;如果没有该元素,则添加进Set。时间复杂度是O(n),但是消耗了O(n)的空间作为代价。

public boolean duplicate(int[] arr) {
   if (arr == null || arr.length == 0)
      return false;

   Set<Integer> set = new HashSet<>();
   for (int i = 0; i < arr.length; i++) {
      if (set.contains(arr[i]))
         return true;
      else
         set.add(arr[i]);
   }
   return false;
}

3、空间复杂度为O(1)的解法
思路:重新分析题干,我们发现:这是一个长度为n的数组,并且元素都在0到n-1之间。也就是说,如果这是数组中没有重复的元素,那么元素中存储的就是数组的下标,只不过可能是乱序的。 我们只需将每一个数字放到它应该存放的下标上即可。
遍历数组,先将当前下标 i 与 arr[i] 比较:

  • 如果不相等,将 arr[i] 与 arr[arr[i]] 比较:
    • 如果不相等,则将这两个数字进行交换。
    • 如果相等,则说明有重复数字。
  • 如果相等
    • 跳过,进入下一轮循环。
      在这里插入图片描述
    public boolean duplicate(int[] arr) {
        if(arr == null || arr.length == 0)
            return false;
        for(int i = 0;i<arr.length;i++) {
//            相等时,说明该数字已经在属于它的位置上。继续下一轮循环。
            while(arr[i] != i) {
//                相等时,说明已找到数组中有相同数字。否则,交换两个数字。
                if(arr[i] == arr[arr[i]]) 
                    return true;
                else {
                    int temp = arr[i];
                    arr[i] = arr[temp];
                    arr[temp] = temp;
                }
            }
        }
        return false;
    }

03_02不修改数组找出重复的数字

在一个长度为n+1的数组里的所有数字都在1到n的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,如果输入长度为8的数组{2, 3, 5, 4, 3, 2, 6, 7},那么对应的输出是重复的数字2或者3。
1、使用O(n)的辅助空间
思路:这一题与上一题很相似,但是题目要求我们不能修改原数组。由于数组长度为n+1,数字范围都在1到n之间,因此我们依然可以根据数组下标来对应数字。我们可以创建一个相同长度的辅助数组。遍历原数组将数字和辅助数组比较,这样很容易发现哪个数字是重复的。因为要创建一个辅助数组,该方法需要O(n)的辅助空间。

	public int getDuplication(int[] arr) {
		if (arr == null || arr.length == 0)
			return -1;
		boolean[] temp = new boolean[arr.length];
		for (int number : arr) {
			if (temp[number])
				return number;
			else
				temp[number] = true;
		}
		return -1;
	}

2、二分法
思路:根据题意,我们得知数组至少有一个重复数字。由于数字都是在1-n范围内的,我们对这个范围进行二分,前一半为1~m,后一半为m+1~n,m=(1+n)/2。

  • 如果在1~m的范围内,数组中1~m的数字超过了m个,说明该范围内有重复数字。此时我们要对这块范围继续二分。
  • 否则,说明重复数在另一个范围,对这个范围进行二分。
  • 当范围缩小到某个数时:
    • 如果数组中这个数的统计数量超过了1个,说明这个数是一个重复数。
    • 否则,出错。
      在这里插入图片描述
	public int getDuplication(int[] arr) {
		if (arr == null || arr.length == 0)
			return -1;
		int start = 1, end = arr.length - 1;
		while (start <= end) {
//			将数字范围一分为二,注意不是根据数组划分的。
			int mid = ((end - start) >> 1) + start;
//			统计数组中属于区域范围的数字。
			int count = count(arr, start, mid);
			if (end == start){
//			范围精确到某个数,并且这个数在数组中有多个,说明找到了一个重复数。
				if(count>1)
					return start;
				else
					break;
			}
//			数组中当前范围内的数字数量大于范围本身,说明该数组中该数字范围有重复数。
			if (count > mid - start + 1) {
				end = mid;
			} else
				start = mid + 1;
		}
//		数据有误。
		return -1;
	}
	
//	统计数字范围内的元素有多少个
	public int count(int[] arr, int start, int end) {
		int count = 0;
		for (int number : arr) {
			if (number >= start && number <= end)
				count++;
		}
		return count;
	}

04_二位数组中的查找

在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

例如:
1	2	8	9
2	4	9	12
4	7	10	13
6	8	11	15
查找数字7,则返回true;如果查找数字5,由于数组不含有该数字,则返回false。

思路:
在二维数组中查找某个数,只要遍历即可。但是这样虽然能够实现,但是并不优雅,也没有充分地利用已知信息。推荐的做法是不断地去缩小范围,直到找到这个数或者发现数组中没有这个数。首先,我们要选取二维数组中某个元素去和目标数进行比较,比较完后还要求能够缩小范围。
我们可以选取二维数组右上角的数(或左下角),不断循环查找。

  • 如果目标数大于它,则目标数肯定不会在这一行中(因为这行中最大的数是选取数),剔除这行(缩小范围),继续查找;
  • 如果目标数小于它,则目标数肯定不会在这一列中(因为这列中最小的数是选取数),剔除这列,继续查找;
  • 如果目标数等于它,则找到了目标数;
  • 如果查找范围为空,说明没有目标数。
    在这里插入图片描述
    public boolean Find(int target, int[][] arr) {
		if(arr == null || arr.length == 0)
			return false;
		int x = 0 , y = arr[0].length-1;
		while (x < arr.length && y >= 0) {
			if (arr[x][y] == target)
				return true;
			else if (arr[x][y] < target)
				x++;
			else
				y--;
		}
		return false;
    }

05_01替换空格

请实现一个函数,把字符串中的每个空格替换成"%20"。例如输入“We are happy.”,则输出“We%20are%20happy.”。
1、从前往后扫描
思路:这是比较普通的做法。我们从头到尾遍历字符串,遇到空格,就把空格后面的字符往后挪动,然后将空格处替换。且不说扩容和越界问题,每次遇到空格我们都需要移动空格后面的字符,因此总的时间复杂度为O(n^2),造成越后面的字符移动次数越多,这显然是多余的。
2、从后往前扫描
思路:既然从前往后扫描的做法不好,那我们就试试从后往前的。

  1. 首先变量一遍字符串,统计空格的数量,由此我们可以得到替换后的字符长度并创建这样一个字符数组。
  2. 从后往前遍历字符串,每当遇到空格,则在新数组的末尾添加“%20”,否则添加原字符。
	public String replaceBlank(String oldStr) {
		if (oldStr == null)
			return null;
			
//		统计空格数量
		int blank = 0;
		for (int i = 0; i < oldStr.length(); i++) {
			if (oldStr.charAt(i) == ' ')
				blank++;
		}
		
//		计算替换后的长度
		int newLen = (blank << 1) + oldStr.length();
		char[] newChar = new char[newLen];

//		从后往前扫描
		for (int i = oldStr.length() - 1; i >= 0; i--) {
			if (oldStr.charAt(i) == ' ') {
				newChar[--newLen] = '0';
				newChar[--newLen] = '2';
				newChar[--newLen] = '%';
			} else {
				newChar[--newLen] = oldStr.charAt(i);
			}
		}
		
		return new String(newChar);

06_从尾到头打印链表

输入一个链表的头结点,从尾到头反过来打印出每个结点的值。链表节点定义如下:

public class ListNode {
	int key;
	ListNode next;
	
	public ListNode(int key) {
		this.key = key;
	}
}

1、使用栈
思路:由于这是一个单向链表,遍历的顺序只能是从头到尾,可输出的顺序却是从尾到头。也就是说,第一个遍历的节点最后一个输出,这是典型的“先进后出”结构,我们可以使用栈来实现。

  • 每遍历到一个节点,将该节点添加进栈。
  • 当遍历完所有节点,再从栈顶开始逐个输出节点的值。
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
	ArrayList<Integer> list = new ArrayList<>();
	if (listNode == null)
		return list;

	LinkedList<Integer> stack = new LinkedList<>();
	while (listNode != null) {
		stack.push(listNode.key);
		listNode = listNode.next;
	}
		
	while (stack.size() != 0)
		list.add(stack.pop());
	return list;
}

2、使用递归
思路:递归在本质上就是一个栈结构,于是又很自然想到用递归来实现。

ArrayList<Integer> list = new ArrayList<>();

public ArrayList<Integer>  printListFromTailToHead(ListNode listNode) {
	if (listNode != null) {
		func(listNode.next);
		list.add(listNode.key);
	}
	return list;
}

07_重建二叉树

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1, 2, 4, 7, 3, 5, 6, 8}和中序遍历序列{4, 7, 2, 1, 5, 3, 8, 6},则重建出如图所示的二叉树并输出它的头结点。二叉树节点的定义如下:

public class TreeNode {
	int value;
	TreeNode left;
	TreeNode right;
	TreeNode root;
	
	public TreeNode(int value) {
		this.value = value;
	}
}

在这里插入图片描述
思路:首先我们要知道的是,在二叉树的前序遍历序列中,第一个数字总是树的根节点的值。但在中序遍历序列中,根节点的值在序列的中间,左子树的节点的值位于根节点的值的左边,而右子树的节点的值位于根节点的值的右边。因此我们要扫描中序遍历序列,才能找到根节点的值。
在这里插入图片描述
既然我们已经分别找到了根节点的左、右子树的前序序列和中序序列,我们可以用同样的方法分别构建左、右子树。也就是说,接下来的事情可以用递归的方法去完成。

public TreeNode reConstructBinaryTree(int[] preOrder, int[] inOrder) {
	if (preOrder == null || preOrder.length == 0
			|| inOrder == null || inOrder.length == 0 
           	|| preOrder.length != inOrder.length)
		return null;

	return reConstruct(preOrder, 0, preOrder.length - 1, inOrder, 0, inOrder.length - 1);
}

/**
*
* @param preOrder	前序序列
* @param preStart	前序序列的左区间 
* @param preEnd		前序序列的右区间 
* @param inOrder	中序序列
* @param inStart	中序序列的左区间
* @param inEnd		中序序列的右区间 
* @return
*/
public TreeNode reConstruct(int[] preOrder, int preStart, int preEnd, int[] inOrder, int inStart, int inEnd) {
	if (preStart > preEnd || inStart > inEnd)
		return null;

	TreeNode node = new TreeNode(preOrder[preStart]);

	for (int i = inStart; i <= inEnd; i++) {
		if (preOrder[preStart] == inOrder[i]) {
			node.left = reConstruct(preOrder, preStart + 1,  preStart + (i - inStart), inOrder, inStart, i - 1);
			node.right = reConstruct(preOrder,  preStart + (i - inStart) + 1, preEnd, inOrder, i + 1, inEnd);
            break;
		}
	}
	return node;
}

08_二叉树的下一个节点

给定一颗二叉树和其中的一个节点,如何找出中序遍历序列的下一个节点?树中的节点除了有两个分别指向左、右子节点的指针,还有一个指向父节点的指针。
在这里插入图片描述

  • 如果一个节点有右子树,那么它的下一个节点就是它的右子树中最左子节点。也就是说,从右子节点出发一直沿着指向左子节点的指针,我们就能找到它的下一个节点。例如,上图中节点b的下一个节点是h,节点a的下一个节点是f。
  • 接着我们分析一个节点没有右子树的情形。如果节点是它父节点的左子树,那么它的下一个节点就是它的父节点。例如,上图中节点d的下一个节点是b,节点f的下一个节点是c。
  • 如果一个节点既没有右子树,并且它还是它父节点的右子节点,那么情形就比较复杂。我们可以沿着指向父节点的指针一直向上遍历,直到找到一个是它父节点的左子节点的节点。如果这样的节点存在,那么这个节点的父节点就是我们要找的下一个节点。
public TreeNode func(TreeNode tree) {
    if (tree == null)
        return null;
//    如果有右子树:则查找右子树的最左节点
    if (tree.right != null) {
        tree = tree.right;
//        一直往下找左子树
        while (tree.left != null)
            tree = tree.left;
        return tree;
    }

//    没有右子树:
    while (tree.root != null) {
//        如果它是它的父节点的左节点,则它的父节点就是下一个节点
        if (tree.root.left == tree)
            return tree.root;
//        如果它是它的父节点的右节点,则循环向上找它的父节点的左节点的是它本身的节点
        else
            tree = tree.root;
    }
    return null;
}

09_用两个栈实现队列

用两个栈实现一个队列。分别完成在队列尾部插入结点和在队列头部删除结点的功能。

首先要插入n个元素,不妨先把它们顺序压入stack1,此时stack1中有n个元素,stack2为空。如果直接从stack1中逐个弹出它们,得到的将是逆序的,但是要求我们实现的是队列,应该是先进先出,即什么顺序进的就什么顺序出。如果我们要把这个逆序的集合的再次逆序,那就得到了原来的顺序。因此我们把stack1中的元素再逐个压入stack2中,再从stack2中弹出,就实现了队列的先进先出效果。

思路总结:

  • 压入:把元素压入一个栈,不妨压入stack1。
  • 弹出:如果stack2不为空,直接从stack2中弹出一个元素。如果stack2为空,则将stack1中所有元素弹出并压入stack2,再从stack2中弹出一个元素。
public class QueueWithTwoStacks<E> {
	private LinkedList<E> stack1 = new LinkedList<>();
	private LinkedList<E> stack2 = new LinkedList<>();
		
	public void push(E e) {
		stack1.push(e);
	}
	
	/**
	 * 出栈时,若stack2不为空,则弹出stack2的一个元素。
	 * 若为空,把stack1的元素全部压入stack2,然后再弹出一个。
	 * 即stack1中的都是新一批元素,而stack2中的都是老一批元素。
	 * @return
	 */	
	public E pop() {
		if (stack2.isEmpty() && stack1.isEmpty())
			throw new RuntimeException("Queue is empty.");
		if (stack2.isEmpty()) {
			while (!stack1.isEmpty())
				stack2.push(stack1.pop());
		}
		return stack2.pop();
	}
}

10_斐波那契数列

写一个函数,输入n,求斐波那契数列的第n项。

1、递归解法

public int Fibonacci(int n) {
	if (n == 1 || n == 2)
		return 1;
	if (n <= 0)
		return 0;
	return func1(n - 1) + func1(n - 2);
}

分析:用递归是最简单的解决方法,但是它有严重的效率问题。我们以求解f(10)为例来分析递归的求解过程。想求得f(10),需要先求得f(9)和f(8)。同样,想求得f(9),需要先求得f(8)和f(7)…我们可以用树形结构来表示。不难发现,这棵树中有很多节点是重复的,而且重复的节点数会随着n的增大而急剧增大。
在这里插入图片描述
2、循环解法
思路:递归的方法之所以慢,是因为重复的计算太多,我们只要想办法避免重复计算就行了。比如把已经得到的数列存储起来。

  • 我们可以从下往上计算,首先根据f(1)和f(2)算出f(3)。以此类推就可以算出第n项了。(时间复杂度O(n),空间复杂度O(1))
public int Fibonacci(int n) {
	if (n <= 0)
		return 0;
	int fir = 0;
	int sec = 1;
	int res = 1;
	for (int i = 2; i <= n; i++) {
		res = fir + sec;
		fir = sec;
		sec = res;
	}
	return res;
}

11_旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3, 4, 5, 1, 2}为{1, 2, 3, 4, 5}的一个旋转,该数组的最小值为1。
1、顺序查找
思路:最简单的方法就是从头到尾遍历数组,找出数组中最小的元素,时间复杂度为O(n)。但是没有充分利用到旋转数组的特性。

public int minNumberInRotateArray(int [] a) {
	if (a == null || a.length == 0)
		return 0;
	for (int i = 0; i < a.length - 1; i++) {
		if (a[i] > a[i + 1])
			return a[i + 1];
	}
	return a[0];
}

2、二分查找
思路:在旋转后数组实际上可以划分为两个排序的子数组,而且前面的数组的元素都大于等于后面的数组的元素。而且最小的元素是后面子数组的第一个元素。在排序数组中我们可以用二分查找法实现O(logn)的查找。
和二分查找法一样,我们用两个指针分别指向数组的第一个元素和最后一个元素。接着我们根据左指针和右指针找出数组的中间元素。

  • 如果中间元素位于前面的递增子数组,那么它应该大于等于左指针指向的元素,此时可以推断出数组最小元素应该位于中间元素和右指针之间。我们可以让左指针指向中间元素,缩小一半的范围,然后继续在这个范围内二分查找。
  • 如果中间元素位于后面的递增子数组,那么它应该小于等于右指针指向的元素,此时可以推断出数组最小元素应该位于左指针和中间元素之间。我们可以让右指针指向中间元素,缩小一半的范围,然后继续在这个范围内二分查找。
  • 还有一种特殊情况,当左右指针和中间元素都相同时,我们是无法判断中间元素是位于前面的子数组还是后面的子数组,也就无法移动左右指针来缩小范围。此时,我们只能采用顺序查找的方法。例如{1,0,1,1,1}和{1,1,1,0,1}
  • 按照上面的思路,左指针总是指向前面的递增数组的元素,而右指针总是指向后面递增数组的元素。最终左指针将指向前面子数组的最后一个元素,而右指针会指向后面子数组的第一个元素,也就是这两个指针会相邻。此时,右指针就是我们要查找的最小元素。
    在这里插入图片描述
public int minNumberInRotateArray(int[] a) {
	if (a == null || a.length == 0)
		return 0;
	if (a.length == 1)
		return a[0];

	int right = a.length - 1;
	int left = 0;
//	初始化为零,避免是排序数组(旋转了零个)
	int mid = 0;

//	如果旋转了零个,跳过循环
	while (a[left] >= a[right]) {
		if (right - left == 1) {
			mid = right;
			break;
		}
        mid = left + ((right - left) >> 1);
        
//		如果三者相同,则无法判断mid是左数组还是右数组的,这种情况只能靠顺序查找
		if (a[mid] == a[left] && a[left] == a[mid])
			return findTheMinNumber(a);
		if (a[mid] >= a[left])
//			不用加一,左右指针分别只会指向左右数组。
			left = mid;
		else
			right = mid;
	}
	return a[mid];
}

/**
顺序查找
**/
public int findTheMinNumber(int[] a) {
	for (int i = 0; i < a.length - 1; i++) {
		if (a[i] > a[i + 1])
			return a[i + 1];
	}
	return a[0];
}

12_矩阵中的路径

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如在下面的3×4的矩阵中包含一条字符串“bfce”的路径。但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。

A B T G
C F C S
J D E H

思路:在选取一个起点后,如果这个元素满足条件则继续往一个方向探索,如果还是满足条件,则继续探索,一旦不满足条件,则回溯到上一个分叉口,换个方向继续探索。就这样不断前进、回溯,直到找到一个满足所有条件的路径,如果所有的方向都不满足,则说明该起点没有此条路径。

/**
 * 
 * @param target	字符矩阵
 * @param rows		矩阵行数
 * @param cols		矩阵列数
 * @param str		目标字符串
 * @return
 */
public static boolean hasPathCore(char[] target, int rows, int cols, String str) {
//	标识当前被访问过的格子。
	boolean[] isVisited = new boolean[target.length];

	for (int i = 0; i < rows; i++) {
		for (int j = 0; j < cols; j++) {
			if (hasPath(target, rows, cols, i, j, 0, str, isVisited))
				return true;
		}
	}
	return false;
}

/**
 * 
 * @param target		字符矩阵
 * @param rows			矩阵行数
 * @param cols			矩阵列数
 * @param row			当前行索引
 * @param col			当前列索引
 * @param k				目标字符串下标索引
 * @param str			目标字符串
 * @param isVisited		矩阵字符是否已被使用
 * @return
 */
public static boolean hasPath(char[] target, int rows, int cols, 
	int row, int col, int k, String str, boolean[] isVisited) {
//	全部匹配
	if (k == str.length())
		return true;

	int index = cols * row + col;

//	跳过超过边界的、已被访问的、不匹配路径的。
	if (row < 0 || col < 0 || row >= rows || col >= cols 
			|| isVisited[index] || str.charAt(k) != target[index])
		return false;

	isVisited[index] = true;

//	回溯,递归寻找一个方向,找不到则回溯回来换个方向继续查找。四个方向都没有则还原。
	if (hasPath(target, rows, cols, row + 1, col, k+1, str, isVisited)
		|| hasPath(target, rows, cols, row - 1, col, k+1, str,isVisited)
		|| hasPath(target, rows, cols, row, col + 1, k+1, str,isVisited)
		|| hasPath(target, rows, cols, row, col - 1, k+1,str,isVisited))
		return true;
//	走到这,说明这一条路不通,还原,再试其他的路径
	isVisited[index] = false;
	return  false;
}

13_机器人的移动范围

地上有一个m行n列的方格。一个机器人从坐标(0, 0)的格子开始移动,它每一次可以向左、右、上、下移动一格,但不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格(35, 37),因为3+5+3+7=18。但它不能进入方格(35, 38),因为3+5+3+8=19。请问该机器人能够到达多少个格子?

思路:该题与上一题非常相似,都是采用的回溯法。

	public int movingCount(int limit, int rows, int cols){
        if (rows <= 0 || cols <= 0 || limit < 0)
        	return 0;
        
        boolean[] marked = new boolean[rows * cols];
        return move( rows, cols, 0, 0, marked, limit);
    }

    public  int move( int rows, int cols, int row, int col, boolean[] marked, int limit) {
        int index = cols * row + col;

//		超过下标或已访问或超过规定条件
        if (row < 0 || col < 0 || row >= rows || col >= cols || marked[index] || !check(row, col, limit)) 
            return 0;

        marked[index] = true;
        return 1 + func2( rows, cols, row - 1, col, marked, limit) + func2( rows, cols, row + 1, col, marked, limit)
                + func2( rows, cols, row, col + 1, marked, limit) + func2( rows, cols, row, col - 1, marked, limit);
    }
    
    public  boolean check(int row, int col, int limit) {
        int temp = 0;
        while (row != 0) {
            temp += row % 10;
            row /= 10;
        }
        while (col != 0) {
            temp += col % 10;
            col /= 10;
        }
        return temp <= limit;
    }

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。

1、动态规划
思路:首先定义函数f(n)为把长度为n的绳子剪成若干段后各段长度乘积的最大值。得出公式f(n) = max(f(i)*f(n-i)),0<i<n。

这是一个从上至下的递归公式,但是递归会有很多重复的子问题。一个更好的办法是按照从下而上的顺序计算,也就是说我们先得到f(2),f(3),在得到f(4),f(5),直到f(n)。

  • 当n < 4:很容易的找出结果,可以作为返回结果。
    如果n < 2,长度不足以剪断,返回0。
    如果n == 2,只能剪成对半,返回1。
    如果n == 3,剪成2*1最佳,返回2。
  • 当n >= 4,情况变得复杂,我们需要借助公式依次计算出f(4),f(5),…f(n)。而每次需要遍历剪断的所有情况,找出其中乘积的最大值,并用数组存起来。
	 int maxProductAfterCutting(int len) {
//		当绳子原长度小于4的情况下,直接返回结果
		if (len < 2)
			return 0;
		if (len == 2)
			return 1;
		if (len == 3)
			return 2;
		
		int[] arr = new int[len + 1];
//		当绳子长度大于4的情况下,以下3种情况不剪比剪要好
		arr[1] = 1;
		arr[2] = 2;
		arr[3] = 3;
		for (int i = 4; i <= len; i++) {
			int max = 0;
//			除二操作可以减少重复运算
			for (int j = 1; j <= i / 2; j++) {
				int temp = arr[j] * arr[i - j];
				max = temp > max ? temp : max;
			}
			arr[i] = max;
		}
		return arr[len];
	}

2、贪心算法
思路:

  • n>=5时,我们尽可能多的剪长度为3的绳子;
  • 当剩下的绳子长度为4时,把绳子剪成两段长度为2的绳子。
int maxProductAfterCutting(int len) {
	if(len < 2)
        return 0;
    if(len == 2)
        return 1;
    if(len == 3)
        return 2;
//	计算最多能剪出多少段长度为3的绳子。
	int timesof3 = len / 3;
//	如果余下一段长度为1的绳子,此时我们拿出一段长度3的绳子合并成长度为4的绳子,再把4平分为两段长度为2的绳子,此时才是最优解。
    if(len % 3 == 1)
        timesof3--;
//	计算剩余的绳子最多能剪出多少段长度为2的绳子。
    int  timesof2 = (len - timesof3 * 3) >> 1; 
    return (int)(Math.pow(2,timesof2) + Math.pow(3,timesof3));
}
         

15_二进制中1的个数

请实现一个函数,输入一个整数,输出该数二进制表示中1的个数。例如把9表示成二进制是1001,有2位是1。因此如果输入9,该函数输出2。

1.可能引起死循环的解法

思路:从右往左逐个与1相与,判断是否为1,然后对整数右移一位

public int NumberOf1_Solution1(int n) {
  	int count = 0;
  	while (n != 0) {
    	if ((n & 1) == 1)
      		count++;
    	n = n >> 1;
  	}
  	return count;
}

分析:如果输入的是正数,那么这段代码能够奏效。但是如果输入的是个负数,那么右移后,最高位会填充1,这就会造成这个数最后所有位都是1,从而进入死循环。对于这种解法的问题解决思路就是,使用无符号右移操作符(即>>>)。

2.常规解法

思路:上面的解法是让输入的数字和1相与,然后对输入的数字进行右移。换种方法,我们也可以让1进行左移,输入的数字保持不变。

public int NumberOf1_Solution2(int n) {
  	int count = 0;
    int temp = 1
  	while (temp != 0) {
    	if ((n & temp) == temp)
      		count++;
    	temp = temp << 1;
  	}
  	return count;
}

分析:这种解法也能够对负数进行计算了。但是对于int,它需要循环32次

3.高效的解法

public static int NumberOf1_Solution3(int n) {
  	int count = 0;
    while(n != 0){
        n++;
        n = n & (n - 1);
    }
    return count;
}

分析:把一个整数减去1,会把这个数最右边的1变成0。如果它的右边还有0,则所有的0都变成1,而它左边的所有位不变。把一个整数减去1之后再和原来的整数做与运算,相当于去掉并统计了最右边的1。

16_数值的整数次方

实现函数double Power(double base ,int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。

1.自以为题目简单的解法

public static double power1(double base, int exp) {
	double res = 1;
	for (int i = 1; i <= exp; i++)
		res *= base;
	return res;
}

分析:只考虑了指数是正整数的情况,负整数没有考虑。

2.全面但不够高效的解法

public static double power2(double base, int exp) throws Throwable {
	if (base == 0 && exp == 0)
		throw new Throwable("0的0次幂没有意义");
	if (base == 0)
		return 0;
	if (exp == 0)
		return 1;

	if (exp < 0)
		return 1 / power1(base, -exp);
	else
		return power1(base, exp);
}

分析:全面考虑了各种情况。但是缺点是指数的数值就是循环的次数,还是不够高效。

3.全面又高效的解法
思路:我们可以这样求解an,利用以下公式来循环求解:

  • 当n为偶数时:an = an/2 *an/2
  • 当n为奇数时:an = an/2 *an/2 * a
public static double pow4(double base, int exponent) {
	if (base == 1)
		return 1;
    
	int exp = exponent;
	double res = 1;

	if (exponent == 0) {
//		0的0次没有意义
		if (base == 0)
			throw new RuntimeException("0的0次方没有意义!");
//		指数为0时,结果为1
		return 1;
	} else if (exponent < 0) {
		exp = -exponent;
	}
		
//	基数为0是,结果为0
	if (base == 0)
		return 0;
		
	/**
	*
	 * 指数为偶数时:res = a^(n-1) * a^(n-1)
	 * 指数为奇数时:res = a^(n-1) * a^(n-1) * a
	 */
	while (exp != 0) {
//		最终exp都会变成1,因此res必定会被赋值!
		if ((exp & 1) == 1)
//			奇数时:还需要再乘一个基数
			res *= base;
//		翻倍
		base *= base;
		exp >>= 1;
	}
//	判断指数正负
	return exponent > 0 ? res : 1 / res;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 《offer JavaPDF》是一本广受欢迎的面试算法解答南。这本书主要针对求职者准备技术面试,特别是Java语言相关的职位。这本书不仅仅提供了问解析,还包含了详细的答案和清晰的思路。这使得读者能够更好地理解问,并掌握解决问的技巧。 这本书采用了PDF格式,具有易于阅读和携带性强的优点。读者可以将其存储在电脑、手机或平板电脑中,随时随地学习和复习。这为读者提供了极大的便利。 《offer JavaPDF》不仅仅是一个算法解答南,还包含了一些求职技巧和面试准备建议。这些内容帮助读者了解面试流程、优化简历、提高面试技巧等,并提供了一些建议来克服可能的挑战。这些经验和建议对求职者来说非常有价值,能够帮助他们在面试中更加出色地表现。 总的来说,《offer JavaPDF》是一本实用的书籍,对于准备技术面试的求职者来说,是一份宝贵的资料。无论是对于算法的解析,还是对于求职技巧的培养,这本书都能提供很多帮助。如果你是一个Java语言的求职者,我强烈建议你阅读这本书,它将为你的面试准备带来很大的帮助。 ### 回答2: Offer是一本非常经典的面试刷南,它包含了很多常见的编程面试目,并提供了详细的解答和解思路。Offer JavaPDF则是将这本书中的目和解答都用Java语言实现,并以PDF文档的形式呈现出来。 这本JavaOffer PDF非常有用,特别适合正在准备面试的程序员。它将面试目按照不同的难度级别进行了分类,并提供了相应的解答和解思路。这样可以帮助程序员更好地了解面试官考察的重点和思考问的方式,提高自己解的能力。 此外,这本JavaOffer PDF还提供了一些常见的算法和数据结构的实现,帮助程序员更好地理解和掌握这些基础知识。通过阅读和实践这些目,程序员可以提高自己的编程能力和解决问的能力,为日后的面试做好充分准备。 总的来说,Offer JavaPDF是一本非常实用的面试刷南,它以Java语言实现了原书中的目和解答,并提供了详细的解思路。它对于准备面试的程序员来说是一本非常有价值的参考书籍,能够帮助他们提高解能力,更好地应对面试挑战。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值