《剑指offer》总结了面试中许多常见的算法题,对于提高编程和算法很有帮助。但是原书是用c语言实现的,我参考原书并加上自己的见解整理了Java版本。为了精简内容,很多细节未能呈现,更完整详细的版本请见我的Github。本人代码小白,正在砥砺前行,考虑如有欠佳,欢迎交流指正。
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() {}
}
在一个长度为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] 比较:
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;
}
在一个长度为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。
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;
}
在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
例如:
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;
}
请实现一个函数,把字符串中的每个空格替换成"%20"。例如输入“We are happy.”,则输出“We%20are%20happy.”。
1、从前往后扫描
思路:这是比较普通的做法。我们从头到尾遍历字符串,遇到空格,就把空格后面的字符往后挪动,然后将空格处替换。且不说扩容和越界问题,每次遇到空格我们都需要移动空格后面的字符,因此总的时间复杂度为O(n^2),造成越后面的字符移动次数越多,这显然是多余的。
2、从后往前扫描
思路:既然从前往后扫描的做法不好,那我们就试试从后往前的。
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);
输入一个链表的头结点,从尾到头反过来打印出每个结点的值。链表节点定义如下:
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;
}
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{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;
}
给定一颗二叉树和其中的一个节点,如何找出中序遍历序列的下一个节点?树中的节点除了有两个分别指向左、右子节点的指针,还有一个指向父节点的指针。
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;
}
用两个栈实现一个队列。分别完成在队列尾部插入结点和在队列头部删除结点的功能。
首先要插入n个元素,不妨先把它们顺序压入stack1,此时stack1中有n个元素,stack2为空。如果直接从stack1中逐个弹出它们,得到的将是逆序的,但是要求我们实现的是队列,应该是先进先出,即什么顺序进的就什么顺序出。如果我们要把这个逆序的集合的再次逆序,那就得到了原来的顺序。因此我们把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();
}
}
写一个函数,输入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、循环解法
思路:递归的方法之所以慢,是因为重复的计算太多,我们只要想办法避免重复计算就行了。比如把已经得到的数列存储起来。
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;
}
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{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)的查找。
和二分查找法一样,我们用两个指针分别指向数组的第一个元素和最后一个元素。接着我们根据左指针和右指针找出数组的中间元素。
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];
}
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如在下面的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;
}
地上有一个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;
}
给你一根长度为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
这是一个从上至下的递归公式,但是递归会有很多重复的子问题。一个更好的办法是按照从下而上的顺序计算,也就是说我们先得到f(2),f(3),在得到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、贪心算法
思路:
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));
}
请实现一个函数,输入一个整数,输出该数二进制表示中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。
实现函数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,利用以下公式来循环求解:
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;
}