(五)剑指offer 巧妙计算篇

本文解析了包括二进制中1的个数、求1+2+3+...+n、不用加减乘除做加法等在内的14道经典编程题目,提供了详细的解题思路与代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

补码

正数的补码是其本身;负数的补码是其反码+1。例如:0000 0001的补码0000 00011000 0001的补码是1111 1111

1.二进制中1的个数

题目

输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

答案

思路:
一个二进制数如果 - 1,则他最右面的 1 会变成 0 ,最右面的 1 的后面的所有 0 会变成 1,比如:1010 1100 - 1 = 1010 0011

所以,n&(n-1)的结果就是去掉最右边的 1 的值,如果用上面的数的话,结果就是1010 1000,也就是每运算一次,去掉一个1。

如果求二进制中1或0的个数的话,还可以每次右移一位,判断是否为零!

	/**
	 * 二进制中1的个数
	 * @param n
	 * @return
	 */
	public static int NumberOf1(int n) {
		int count = 0;
		while(n != 0) {
			n = n & (n - 1);
			count++;
		}
		return count;
	}

2.求1+2+3+…+n

题目

求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

答案

思路:知道&&运算符左边不成立,不会运算右边就很好做。

	/**
	 * 用if条件判断实现
	 * @param n
	 * @return
	 */
    public static int f(int n) {
    	if(n == 0) {
    		return 0;
    	}else {
    		return f(n -1) + n;
    	}
    }
    
	/**
	 * 三元条件判断符实现
	 * @param n
	 * @return
	 */
    public static int f(int n) {
    	return n > 0 ? (f(n - 1) + n) : 0;
    }
    
    /**
     * 采用&&运算符
     * @param n
     * @return
     */
    public static int Sum_Solution(int n) {
    	int sum = n;
    	boolean sign = n > 0 && (sum += Sum_Solution(n - 1)) != 0;
    	return sum;
    }

3.不用加减乘除做加法

题目

写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

答案

思路:看讨论区大佬说的!

首先看十进制是如何做的: 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为最终结果。

	/**
	 * 递归实现
	 * @param num1
	 * @param num2
	 * @return
	 */
	public static int Add(int num1, int num2) {
		if (num2 == 0)
			return num1;
		else
			return Add(num1 ^ num2, (num1 & num2) << 1);

	}
	/**
	 * 循环实现
	 * @param num1
	 * @param num2
	 * @return
	 */
	public static int Add(int num1, int num2) {
		int sum = num1;
		while(num2 != 0) {
			sum = num1 ^ num2;
			num2 = (num1 & num2) << 1;
			num1 = sum;
		}
		return sum;
	}

4.旋转数组的最小数字

题目

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

答案

思路:剑指Offer中有这道题目的分析。这是一道二分查找的变形的题目。

旋转之后的数组实际上可以划分成两个有序的子数组:前面子数组的大小都大于后面子数组中的元素

注意到实际上最小的元素就是两个子数组的分界线。本题目给出的数组一定程度上是排序的,因此我们试着用二分查找法寻找这个最小的元素。

思路:

(1)我们用两个指针left,right分别指向数组的第一个元素和最后一个元素。按照题目的旋转的规则,第一个元素应该是大于最后一个元素的(没有重复的元素)。

但是如果不是旋转,第一个元素肯定小于最后一个元素。

(2)找到数组的中间元素。

中间元素大于第一个元素,则中间元素位于前面的递增子数组,此时最小元素位于中间元素的后面。我们可以让第一个指针left指向中间元素。

移动之后,第一个指针仍然位于前面的递增数组中。

中间元素小于第一个元素,则中间元素位于后面的递增子数组,此时最小元素位于中间元素的前面。我们可以让第二个指针right指向中间元素。

移动之后,第二个指针仍然位于后面的递增数组中。

这样可以缩小寻找的范围。

(3)按照以上思路,第一个指针left总是指向前面递增数组的元素,第二个指针right总是指向后面递增的数组元素。

最终第一个指针将指向前面数组的最后一个元素,第二个指针指向后面数组中的第一个元素。

也就是说他们将指向两个相邻的元素,而第二个指针指向的刚好是最小的元素,这就是循环的结束条件。

到目前为止以上思路很耗的解决了没有重复数字的情况,这一道题目添加上了这一要求,有了重复数字。

因此这一道题目比上一道题目多了些特殊情况:

我们看一组例子:{1,0,1,1,1} 和 {1,1, 1,0,1} 都可以看成是递增排序数组{0,1,1,1,1}的旋转。

这种情况下我们无法继续用上一道题目的解法,去解决这道题目。因为在这两个数组中,第一个数字,最后一个数字,中间数字都是1。

第一种情况下,中间数字位于后面的子数组,第二种情况,中间数字位于前面的子数组。

因此当两个指针指向的数字和中间数字相同的时候,我们无法确定中间数字1是属于前面的子数组(绿色表示)还是属于后面的子数组(紫色表示)。

也就无法移动指针来缩小查找的范围。

    /**
     * 旋转数组的最小数字
     * @param array
     * @return
     */
    public int minNumberInRotateArray(int [] array) {
       int left = 0;
       int right = array.length - 1;
       int mid;
       while(left < right) {
    	   if(right - left == 1)
    		   break;
    	   mid = (left + right) / 2;
    	   if(array[mid] < array[left]) 
    		   right = mid;
    	   else
    		   left = mid;
       }
       return array[right];
    }

5.整数中1出现的次数

题目

求出1 - 13 的整数中1出现的次数,并算出100 - 1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。

答案

思路:剑指讨论区大佬的解释,直接借用过来,方便以后复习,啦啦啦。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

	/**
	 * 整数中1的个数
	 * @param n
	 * @return
	 */
	public static int NumberOf1Between1AndN_Solution(int n) {
		if(n <= 0)
			return 0;
		int count = 0;
		for(int i = 1; i <= n; i *= 10) 
			count += (n / (i * 10)) * i + Math.max(Math.min(n % (i * 10) - i + 1, i), 0);
		return count;
	}

6.扑克牌顺子

题目

LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张_)…他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子…LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。

答案

思路:
先不考虑有大小王的情况:
五个数的顺子,所以数组只能有五个数;不能出现对子(重复数字);顺子的最大数减顺子的最小数只能等于四;只要满足这三个条件的数组,都可以组成顺子。
现在考虑有大小王的情况:
数组只能有五个数;不能有对子(除0之外的重复数字);最大数减最小数小于等于四;

	/**
	 * 扑克牌顺子
	 * @param numbers
	 * @return
	 */
	public boolean isContinuous(int[] numbers) {
		if(numbers.length != 5)
			return false;
		HashMap< Integer, Integer> map = new HashMap<>();
		int max = 1;
		int min = 13;
		for(int i = 0; i < numbers.length; i++) {
			if(numbers[i] == 0) {
				continue;
			}else if(map.containsKey(numbers[i])) {
				return false;
			}else {
				map.put(numbers[i], 0);
				min = Math.min(min, numbers[i]);
				max = Math.max(max, numbers[i]);
			}
		}
		if(max - min < 5) 
			return true;
		return false;
	}

7.孩子们的游戏(圆圈中最后剩下的数)

题目

每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0…m-1报数…这样下去…直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!_)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)

答案

思路:
用循环链表,模仿游戏的整个过程,可以自己实现各循环链表,但是这样太麻烦,所以就用单链表或者双链表取余的方式达到循环链表的效果。

主要想说的是这行代码sd = (sd + m - 1) % list.size();,第一次删掉的位置是从0开始数m-1个位置, 以后每次从删掉的下一个节点开始取, 所以每次要在sd的索引处加上m-1,因为是模仿环,所以加了以后对链表长度取余,从而达到想要的效果。

还有一点,你可能会有疑问,报数是从0 - (m-1),总共m个数,为什么上面那行代码要加m-1,而不是m,因为list.remove(sd);删除是从0 - sd删除的,删除的是第sd + 1个元素,元素的索引是sd,所以这里用m-1,而不是m

	/**
	 * 孩子们的游戏(圆圈中最后剩下的数) 
	 * @param n
	 * @param m
	 * @return
	 */
	public static int LastRemaining_Solution(int n, int m) {
		LinkedList<Integer> list = new LinkedList<>();
		for (int i = 0; i < n; i++)
			list.add(i);
		// 每一次开始的下标,也是下一次删除的索引
		int sd = 0;
		while (list.size() > 1) {
			sd = (sd + m - 1) % list.size();
			list.remove(sd);
		}
		return list.size() == 1 ? list.get(0) : -1;
	}

8.替换空格

题目

请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

答案
	/**
	 * 替换空格
	 * @param str
	 * @return
	 */
	public static String replaceSpace(StringBuffer str) {

		StringBuffer newSB = new StringBuffer();
		for (int i = 0; i < str.length(); i++) {
			if (str.charAt(i) != ' ') {
				newSB.append(str.charAt(i));
			} else {
				newSB.append("%20");
			}
		}
		return newSB.toString();
	}

9.斐波那契数列

题目

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。
n<=39

斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从1963年起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。

答案

思路:
(1)递归实现
因为只知道第一项和第二项,后面的都是前两项的和,所以使用递归来做就很简单了,但是使用递归的效率很感人,比如你想知道 f(10) ,就要算出 f(9) 和 f(8),想要知道 9 就要算出 8 和 7, 依次往下计算,会出现很多重复的计算,就很难受,所以只要明白递归的实现方法,和为什么不这么用就ok,不推荐用递归去解决这道题。

	/**
	 * 斐波那契
	 * @param n
	 * @return
	 */
	public static int Fibonacci(int n) {
		if(n == 0)
			return 0;
		if(n == 1)
			return 1;
		return Fibonacci(n - 1) + Fibonacci(n - 2);
	}

(2)循环实现
采用循环实现就很奈斯,时间发复杂度为O(n),也很简单。

	/**
	 * 斐波那契数列循环实现
	 * @param n
	 * @return
	 */
	public static int Fibonacci(int n) {
		if(n == 0)
			return 0;
		if(n == 1)
			return 1;
		int temp = 0;
		int n1 = 0;
		int n2 = 1;
		for(int i = 3; i <= n; i++) {
			temp = n2;
			n2 = n2 + n1;
			n1 = temp;
		}
		return n1 + n2;
	}

10.跳台阶

题目

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

答案

思路:
只要台阶数超过三层,那么跳上该层台阶n只有两种可能,要么是该台阶的下一层n-1,要么是下下一层n-2,所以铁子,你是不是已经想到用“斐波那契数列”数列来解决了啊!!!

	/**
	 * 跳台阶
	 * @param target
	 * @return
	 */
	public static int JumpFloor(int target) {
		if(target <= 0)
			return 0;
		if(target == 1)
			return 1;
		if(target == 2)
			return 2;
		int tar1 = 1;
		int tar2 = 2;
		int temp = 0;
		for(int i = 4; i <= target; i++) {
			temp = tar2;
			tar2 += tar1;
			tar1 = temp;
		}
		return tar1 + tar2;
	}

11.变态跳台阶

题目

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

答案

思路:
在这里插入图片描述假设第n层的跳法为f(n)

f(1)= 1;因为只有从第0层跳上去
f(2)= f(1)+1 = 2;只有从第1层、第0层跳上去
f(3)= f(2)+ f(1)+1 = 4;只有从第2层、第1层、第0层跳上去。
f(4)= f(3)+f(2)+f(1)+ 1 = 8;
……
很容易发现,f(n)是一个以1为第一项,2为公比的等比数列,f(n)= 2 * f(n-1)

这不就出来了嘛,铁子!!!

	/**
	 * 变态跳台阶
	 * 
	 * @param target
	 * @return
	 */
	public static int JumpFloorII(int target) {
		if(target < 1)
			return 0;
		int res = 1;
		for(int i = 2; i <= target; i++) 
			res = res * 2;
		return res;
	}

12.矩形覆盖

题目

我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

答案

思路:
刚开始,我还想着,给一个n,求出被若干个1、若干个2的和组成的可能数,但是想了一下,这个和跳台阶的题很像啊,我做那个题的时候,刚开始的思路也是这样的,然后就想到了斐波那契数列。
在这里插入图片描述如果n=7,第7个格子是竖着放的(第二个表格),则这样的摆放可能数等于前面6个格子的可能数。
第7个格子是横着放的(第一个表格),这样的摆放可能数等于前面5个格子的可能数,所以,前7个格子摆放的可能总数等于前6个的总数和前5个的总数之和。斐波那契有没有!!!0

	/**
	 * 矩形覆盖
	 * @param target
	 * @return
	 */
	public int RectCover(int target) {
		int n1 = 1;
		int n2 = 2;
		int temp = 0;
		if(target < 1)
			return 0;
		if(target == 1)
			return n1;
		if(target == 2)
			return n2;
		for(int i = 4; i <= target; i++) {
			temp = n2;
			n2 = n1 + n2;
			n1 = temp;
		}
		return n1 + n2;
    }

13.数值的整数次方

题目

给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

答案

思路:
这道题感觉没什么好说的,就是防止分母不能为零就OK!

	/**
	 * 数值的整数次方 
	 * @param base
	 * @param exponent
	 * @return
	 */
	public static double Power(double base, int exponent) {
        double res = 1;
        int n = exponent;
        
        if(n < 0 && base != 0)
        	n = -n;
        if(n < 0 && base == 0)
        	throw new RuntimeException( "分母不能为零!");
        if(n == 0 && base == 0)
        	return 1;
        
        for(int i = 1; i <= n; i++) 
        	res *= base;
        res = exponent > 0 ? res : 1 / res;
        return res;
	  }

14.顺时针打印矩阵

题目

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.

答案
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yelvens

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值