文章目录
链表反转I(热身题)
代码
class Solution {
public ListNode reverseList(ListNode head) {
//健壮性
if(head==null){
return null;
}
ListNode prev=head;
ListNode current=head.next;
//它最后肯定是末节点
prev.next=null;
while(current!=null){
//记录反转节点的下一个节点
ListNode next=current.next;
//反转节点
current.next=prev;
//两个整体向右平移
prev=current;
current=next;
}
return prev;
}
}
反转链表II(字节跳动面试真题)
注意
这里需要用到头结点,因为如果left==1的话,head节点会被反转,返回的就不是这个head了,为了方便统一返回值,我们就增加一个头结点指向head结点,然后这时候返回的肯定就是头结点的next节点了。
代码
class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
//m等于n的时候以及head为空的时候无操作
if (head == null || m == n) {
return head;
}
//声明头结点
ListNode dummy = new ListNode(-1);
dummy.next = head;
//找到要反转的第一个节点的前一个节点,等会会改变它的next指向
head = dummy;
for (int i = 1; i < m; i++) {
head = head.next;
}
ListNode prevM = head;
ListNode mNode = head.next;
ListNode nNode = mNode;
ListNode postN = nNode.next;
//上面这几句代码跟上道题的这两句代码效果差不多
// nNode => prev;
//postN => current
//下面这个反转过程和上道题一模一样
for(int i = m; i < n; i++) {
/**
ListNode next = current.next;
current.next = prev;
prev = current;
current = next;
**/
ListNode next = postN.next;
postN.next = nNode;
nNode = postN;
postN = next;
}
//反转完成后postN是被反转的最后一个节点的下一个节点
//nNode是被反转的最后一个节点,它成为了新的第一个节点
//被反转的第一个节点next指向最右边的被反转的next节点
mNode.next = postN;
//最左边的的反转节点的前一个节点的next指向反转完成后的第一个节点即nNode
prevM.next = nNode;
return dummy.next;
}
}
链表相加
图片理解过程
每一轮l1和l2的val相加,用一个数保存进位的数,新建一个节点来保存相加后对10取模的值,最后返回这个链表。
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
//carry来保存进位的数
int carry = 0;
//头结点head
ListNode head = new ListNode(-1);
ListNode pre = head;
while (l1 != null && l2 != null) {
int number = l1.val + l2.val + carry;
carry = number / 10;
ListNode node = new ListNode(number % 10);
pre.next = node;
pre = pre.next;
//两个都往下一个跳
l1 = l1.next;
l2 = l2.next;
}
//如果l1比l2长,相加操作跟上面一样
while (l1 != null) {
int number = l1.val + carry;
carry = number / 10;
ListNode node = new ListNode(number % 10);
pre.next = node;
pre = pre.next;
l1 = l1.next;
}
//如果l2比l1长
while (l2 != null) {
int number = l2.val + carry;
carry = number / 10;
ListNode node = new ListNode(number % 10);
pre.next = node;
pre = pre.next;
l2 = l2.next;
}
//最后还是可能进最后一位
if (carry != 0) {
ListNode node = new ListNode(carry);
pre.next = node;
}
return head.next;
1.数组中重复的数字
题目描述
在一个数组长度为n的数组里,所有数字都在 0 ~ n-1 范围内,数组中某些数字是重复的,但不知道几个数字重复了,也不知道每个数字重复了多少次,请输出数组中任意重复的数字。
三种解法
(1)将数组排序后扫描一遍即可,给一个长度为n的数组排序时间复杂度最好为O(nlogn),这里就不上代码了。(逼格最低)
(2)利用哈希表(空间换时间的思想)
思路:建立a(用于输入数据)的数组的同时建立一个b(用于记录是否有重复数字)的辅助数组,a数组输入数据的同时b数组来做记录并且每次a数组输入数据时b数组都要来判断是否已经输入过这个数字了(例如输入a[0]=1时,先判断,若b[1]=0,则设b[1]=1,说明1这个数字已经出现过一次了,若b[1]=1,则说明这个数字已经输入过了,随便拿个变量记录下来就可以了)
优点:时间复杂度为O(n),更快了
缺点:空间复杂度为O(n),变大了
这个思路也很简单,就不上代码了
(3)剑指offer解法(逼格最高)
思路:数组的数字都是在0-n-1范围,如果没有重复数字,数组排序后,数字i就会出现在数组下标为i的位置上,如果有重复数字,那么就至少有一个数字x不是出现在数组下标为x的位置上。
代码流程:
扫描下标为i的数字(用m表示),先比较它是否等于i,若是说明它已经在它应该的位置,不作任何处理,若不是,则拿它与下标为m的数字比较。若它和第m个数字相等,则找到了第一个重复的数字(因为该数字在下标为i和m的地方都出现了),如果它与第m个数字不等,则把它与第m个数字交换,将m放在下标为m的位置,接下来又重复比较(即现在的m换成了交换之前的第m个数字)
上代码
public boolean real(int[] a){
for(int i=0;i<a.length;i++){
while(a[i]!=i){
if(a[i]==a[a[i]]){
//shit是我这个类的一个变量,记录第一个发现的重复数字
this.setShit(a[i]);
return true;
}
//注意这里的交换,很容易出错
int swap=a[i];
a[i]=a[swap];
a[swap]=swap;
}
}
return false;
}
说明:
虽然是双重循环,但是要么是while一直在循环排序,要么while就只执行一次(for来进行排序)。所以时间复杂度是O(n),而且空间复杂度为O(1) ,所以这个B格最高。
2.不修改数组找出重复的数字
题目描述
在一个长度为n+1的数组里的所有数字都在1-n的范围内,所以数组至少有一个重复的数字。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,输入长度为8的数组{2,3,5,4,3,2,6,7},那么对应的输出是重复的数字2或者3
两种解法
(1)创建一个len为n+1的辅助数组,把每一个原数组的值拷贝到辅助数组,(例子:如果原数组中被复制的数字是m,那么就把它复制到辅助数组中下标为m的位置)这样就能很容易发现哪个数字是重复的。
优点: 时间复杂度为O(n)。
缺点:需要O(n)的辅助空间。
(2)剑指offer解法
分析:1-n的范围内只有n个数字,如果数组超过n个数字,就肯定包含重复数字。可以看出一个范围内的数字的个数是一个关键线索。
解答思路:把1-n的数字从中间的数字m分为两部分,前面一半为1-m,后面一般为m+1-n。如果1-m的数字的数目超过m个,那么这个区间内一定包含重复数字:否则,另一半区间包含重复数字。我们可以继续把包含重复数字的区间一分为二,直到找到一个重复的数字。这个过程跟二分查找算法差不多,只是多了一步统计区间里的数字的数目。
代码
class Solution {
public int findDuplicate(int[] nums) {
int len = nums.length;
int left = 1;
int right = len - 1;
while (left < right) {
int mid = left + (right - left) / 2;
//计数
int count = 0;
//遍历整个nums数组
for (int num : nums) {
//如果属于这个区间的数字,出现次数就加1
if (num <= mid) {
count++;
}
}
if (count > mid) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
}
说明
外面的循环时间复杂度为O(logn),里面的时间复杂度为O(n),一起的话就是O(nlogn),空间复杂度为O(1)。跟前面的解法相比就是时间换空间。
特别注意
这种算法不能保证找出所有重复的数字,因为此算法不能确定是每个数字各出现一次还是某个数字恰好出现了应等的区间的次数。
所以要根据面试官的功能要求和性能要求来选择使用哪种解决方案。
3.二维数组中的查找
题目描述
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:
每行的元素从左到右升序排列。
每列的元素从上到下升序排列。
示例:
现有矩阵 matrix 如下:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。
给定 target = 20,返回 false。
普通思路
选取一个点开始,判断它和目标数字的大小关系,若大于则去左边或者上边找,若小于则去右边或者下边找。但是这种方法的问题是判断后继续去找的是两个区域,而且这两个区域还有重叠
正确思路
选取右上角或者左下角的点开始寻找,若大于目标数字则去该点的行里面找,若小于目标数字则去该点的列找。若等于则结束查找过程。一直这样循环就能找到或者查找为空。(这种方法就是每一次判断后都只会继续去一个区域找,会自动剔除一个区域,因为左下角是列最大的元素,右上角是列最小的元素。)
代码
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
boolean truth=false;
if(matrix==null||matrix.length==0||matrix[0].length==0){
return false;
}
int rows=matrix.length;
int columns=matrix[0].length;
int row=0;
int column=columns-1;
while(row<rows&&column>=0){
if(matrix[row][column]==target){
truth=true;
break;
}
else if(matrix[row][column]<target){
row++;
}
else{
column--;
}
}
return truth;
}
}
4.替换空格
题目描述
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
示例 1:
输入:s = "We are happy."
输出:"We%20are%20happy."
限制:
0 <= s 的长度 <= 10000
两种解法
第一种
从头扫描字符串遇到空格,将后面的所有字符后移两个单位。
优点:不需要额外新增辅助空间
缺点:时间复杂度为O(n)
第二种
第一遍先扫描字符串,记录有多少个空格(count),然后申请一个长度为该字符串的长度加上2*count的辅助字符数组,然后再遍历字符串,边遍历边给辅助数组赋值,同时遇到空格辅助字符数组就单独给字符赋值,最后将辅助数组转化为字符串返回即可
优点:时间复杂度为O(n)
缺点:空间复杂度为O(n)
代码
class Solution {
public String replaceSpace(String s) {
if(s==null) {
return null;
}
int count=0;
for(int i=0;i<s.length();i++) {
if(s.charAt(i)==' ') {
count++;
}
}
char[] real=new char[s.length()+2*count];
for(int i=0,j=0;i<s.length();i++,j++) {
if(s.charAt(i)==' ') {
real[j++]='%';
real[j++]='2';
real[j]='0';
}
else {
real[j]=s.charAt(i);
}
}
String newStr=new String(real);
return newStr;
}
}
5.从尾到头打印链表
题目描述
输入一个链表的头结点,从尾到头反过来打印出每个节点的值。
三种解法
1.反转链表
思路:直接遍历链表,边遍历边反转链表。
代码
class Solution {
public int[] reversePrint(ListNode head) {
//核心代码
ListNode pre=null;
ListNode cur=head;
int sum=0;
while(cur!=null){
ListNode curNext=cur.next;
cur.next=pre;
pre=cur;
cur=curNext;
sum++;
}
//
int[] reverseOrder=new int[sum];
int i=0;
while(pre!=null){
reverseOrder[i++]=pre.val;
pre=pre.next;
}
return reverseOrder;
}
}
注意:反转链表的核心代码可以记下来,自己写可能会写的很复杂,此方法改变了链表的结构
2.入栈法
思路:输出的顺序与逻辑顺序是反的,就很容易想到栈的先进后出的特性。就可以遍历链表,边遍历边入栈。
代码
public class Solution {
public int[] reversePrint(ListNode head) {
Stack<Integer> real= new Stack<>();
int sum=0;
while(head!=null){
real.push(head.val);
sum++;
head=head.next;
}
int[] order=new int[sum];
int i=0;
while(!real.empty()){
order[i++]=real.pop();
}
return order;
}
}
此方法空间复杂度为O(n)
3.递归法
思路:学过树的遍历可以知道用递归,如果有下一个节点就递归,若无则输出
public class Solution {
public void print(ListNode head) {
if(head!=null){
if(head.next!=null){
print(head.next);
}
System.out.println(head.val);
}
}
}
此方法在链表非常长的时候,会导致函数调用的层级很深,从而可能导致函数调用的栈溢出
注意:要根据面试官的要求来选择解法(时间优先,空间优先,能否改变链表的结构)。
6.重建二叉树
题目描述
输入某二叉树的前序遍历和中序遍历的结果,重建该二叉树
思路
前序遍历的第一个点是根节点,再去中序遍历找这个点,然后中序遍历在这个点左边的为左子树的点,右边的为右子树的点,用这个思路递归就成功重建了该二叉树。
代码
class Solution {
Map<Integer, Integer> indexMap ;
public TreeNode buildTree(int[] preorder, int[] inorder) {
indexMap=new HashMap<>();
if (preorder == null || preorder.length == 0) {
return null;
}
int length = preorder.length;
for (int i = 0; i < length; i++) {
indexMap.put(inorder[i], i);
}
TreeNode root = buildTree(preorder, 0, length - 1, inorder, 0, length - 1);
return root;
}
public TreeNode buildTree(int[] preorder, int preorderStart, int preorderEnd, int[] inorder, int inorderStart, int inorderEnd) {
if (preorderStart > preorderEnd) {
return null;
}
int rootVal = preorder[preorderStart];
TreeNode root = new TreeNode(rootVal);
if (preorderStart == preorderEnd) {
return root;
}
else {
int rootIndex = indexMap.get(rootVal);
int leftNodes = rootIndex - inorderStart, rightNodes = inorderEnd - rootIndex;
TreeNode leftSubtree = buildTree(preorder, preorderStart + 1, preorderStart + leftNodes, inorder, inorderStart, rootIndex - 1);
TreeNode rightSubtree = buildTree(preorder, preorderEnd - rightNodes + 1, preorderEnd, inorder, rootIndex + 1, inorderEnd);
root.left = leftSubtree;
root.right = rightSubtree;
return root;
}
}
}
参考第二种解法
注意:重点想清楚传的参数
7.二叉树的下一个节点
题目描述
给定一棵二叉树和其中的一个节点,如何找出中序遍历序列的下一个节点?树中的节点除了有两个分别指向左,右子节点的指针,还有一个指向父节点的指针。
图
思路
如果一个节点有右子树,那么这个节点的下一个节点就是该右子树的最左节点,
如果一个节点没有右子树并且该节点是它的父节点的左子节点,那么该节点的下一个节点就是它的父节点,
如果一个节点没有右子树且它是它父节点的右子节点,这种情况最复杂,我们可以顺着指向父节点的指针一直向上遍历,知道找到一个是它父节点的左子节点的节点。如果这样的节点存在,那么这个节点的父节点就是我们要找的下一个节点。
代码
TreeNode MyFindNext(TreeNode pNode)
{
if(pNode==null)return null;
TreeNode next=null;
if(pNode.right!=null){
TreeNode rightNode=pNode.right;
while(rightNode.left!=null){
rightNode=rightNode.left;
}
next=rightNode;
}
//这一句其实是判断是否是根节点,若是并且上面判断了右子树为空,则说明没有下一个节点了
else if(pNode.parent!=null){
TreeNode cur=pNode;
TreeNode curPar=pNode.parent;
//不进入循环可能是curPar为空,这种情况说明没有下一个节点了
//若是cur!=curPar.right的情况,则说明找到了下一个节点了
while(curPar!=null&&cur==curPar.right)
{
cur=curPar;
curPar=curPar.parent;
}
next=curPar;
}
return next;
}
8.用两个栈实现队列
题目描述
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
思路
一个栈用来push数据,一个栈用来pop数据,若pop的栈和push的栈都为空则说明不能pop,若pop的栈为空且push的栈不为空,则将push栈里面的数据全部装入pop栈中,且数据的顺序也变成了先进先出。若pop栈不为空则直接pop
代码
class CQueue {
Stack<Integer> in;
Stack<Integer> out;
public CQueue() {
in=new Stack<>();
out=new Stack<>();
}
public void appendTail(int value) {
in.push(value);
}
public int deleteHead() {
if(out.empty()){
while(!in.empty()){
out.push(in.pop());
}
}
if(!out.empty()){
return out.pop();
}
return -1;
}
}
9.斐波那契数列
题目描述
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
两种解法
1.递归
教科书的解法,能很快写出。
2.循环
由上图知道了,很多计算其实是重复的,所以可以用循环记录已经计算过的结果。
代码
class Solution {
public int fib(int n) {
if(n==1){
return 1;
}
if(n==0){
return 0;
}
long second=1;
long first=0;
long sum=0;
for(int i=2;i<=n;i++){
sum=second+first;
//注意是在循环里面去取余
sum=sum%1000000007;
//这句一定要放在second=sum之前
first=second;
second=sum;
}
return (int)sum;
}
}
10.青蛙跳台阶问题
题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入:n = 2
输出:2
示例 2:
输入:n = 7
输出:21
提示:
0 <= n <= 100
思路
把n级台阶的跳法总数看为n的函数,记f(n),当n>2时,第一次跳有两种不同的选择:第一种是第一次跳一级台阶,此时后面的跳法总数为n-1个台阶的跳法总数,第二种是第一次跳2级台阶,此时跳法总数等于后面的n-2个台阶的跳法总数,即f(n-2),所以n阶台阶的跳法总数为f(n)=f(n-1)+f(n-2)。这样其实就变成了斐波那契数列的问题了。
代码
class Solution {
public int numWays(int n) {
if(n==1||n==2)return n;
if(n==0)return 1;
int f1=1;
int f2=2;
int sum=0;
for(int i=3;i<=n;i++){
sum=f1+f2;
sum=sum%1000000007;
f1=f2;
f2=sum;
}
return sum;
}
}
11.旋转数组的最小数字
题目描述
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。
示例 1:
输入:[3,4,5,1,2]
输出:1
示例 2:
输入:[2,2,2,0,1]
输出:0
两种解法
1.遍历数组
显然遍历数组可以找出最小值,但是这种方法没有利用旋转数组的特性,时间复杂度为O(n)
2.二分查找
利用旋转数组的特性进行二分查找。理想情况下旋转数组是前半部分大,后半部分小,且(low,high,mid不相等)例如3,4,5,1,2是1,2,3,4,5旋转而来的。不理想的情况即1,0,1,1,1和1,1,1,0,1由0,1,1,1,1旋转而来,这种情况特殊在于(low,high,mid都相等)不能常规的用二分查找。
代码
class Solution {
public int minArray(int[] numbers) {
int low = 0;
int high = numbers.length - 1;
while (low < high) {
//不用high+low是防止溢出
//也可以这样写 int pivot=(low+high)>>2;
int pivot = low + (high - low) / 2;
//说明privot在后半部分较小的里面,它也可能是最小值或者最小值在它前面
if (numbers[pivot] < numbers[high]) {
high = pivot;
}
//说明privot在前半部分较大的里面,最小值在它后面
else if (numbers[pivot] > numbers[high]) {
low = pivot + 1;
}
//说明不是理想的情况,low,mid,high都相等了。
else {
high -= 1;
}
}
return numbers[low];
}
}
12.矩阵中的路径
题目描述
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。
[[“a”,“b”,“c”,“e”],
[“s”,“f”,“c”,“s”],
[“a”,“d”,“e”,“e”]]
但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。
示例 1:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
示例 2:
输入:board = [[“a”,“b”],[“c”,“d”]], word = “abcd”
输出:false
提示:
1 <= board.length <= 200
1 <= board[i].length <= 200
思路
利用回溯法解决,任挑一个起始点,然后dfs它去找下一个,一直找,若找不到就返回false,交给下一个去dfs,若找到了,返回true,程序结束
代码
class Solution {
public boolean exist(char[][] board, String word) {
char[] words=word.toCharArray();
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
if(dfs(board,words,i,j,0)){
return true;
}
}
}
return false;
}
//k是记录已经成功匹配到了第几个了
public boolean dfs(char[][] board,char[] word,int i,int j,int k){
//这里先验证字符是否正确,正确才能往下走,k才+1
if(i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]) {
return false;
}
//匹配完成
if(k == word.length - 1) {
return true;
}
char tmp = board[i][j];
//修改值,防止二次匹配
board[i][j] = '/';
boolean res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) ||
dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);
//修改回来
board[i][j] = tmp;
return res;
}
}
13.机器人的运动范围
题目描述
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
示例 1:
输入:m = 2, n = 3, k = 1
输出:3
示例 2:
输入:m = 3, n = 1, k = 0
输出:1
提示:
1 <= n,m <= 100
0 <= k <= 20
思路
依然是回溯法,很简单,不解释过多了
代码
public class Solution {
public int movingCount(int m, int n, int k) {
if(k==0){
return 1;
}
//切记使用boolean类型的变量,因为它有初始默认值,而Boolean是包装类没有默认值
boolean[][] visited=new boolean[m][n];
return nextStep(0,0,k,visited,m,n);
}
//dfs算法,寻找下一步
public int nextStep(int nowM,int nowN,int k,boolean[][] visited,int m,int n){
if(nowM<0||nowM>m-1){
return 0;
}
else if(nowN<0||nowN>n-1){
return 0;
}
//此处要注意是每个数字每个位置相加,不是直接数字相加
else if((nowM/10+nowM%10+nowN/10+nowN%10)>k){
return 0;
}
else if(visited[nowM][nowN]){
return 0;
}
else{
visited[nowM][nowN]=true;
return nextStep(nowM+1,nowN,k,visited,m,n)+nextStep(nowM-1,nowN,k,visited,m,n)+nextStep(nowM,nowN+1,k,visited,m,n)+nextStep(nowM,nowN-1,k,visited,m,n)+1;
}
}
}
14.剪绳子
题目描述
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
提示:
2 <= n <= 58
两种解法
1.动态规划
从下至上递推,f(2)只能剪成1和1这种,所以f(2)=1,f(3)可能剪成1,2或者1,1,1这两种,因此f(3)=2。
第一刀可以剪i的长度,第二刀就是n-i的长度,f(n)是存储这个长度剪后结果最大的值,我们就可以递推了。
公式:f(n)=max( f(i)*f(n-i))
时间复杂度:O(n2)
空间复杂度:O(n2)
代码
class Solution {
public int cuttingRope(int n) {
if(n==2)return 1;
if(n==3)return 2;
int[] lens=new int[n+5];
//这三行是大于等于4的情况,跟n<=3不同,它是可以分割为n这么大,4可以分解成1和3这俩段,也可以2和2这俩段
//但是这时不会再分了,因为再分的话反而更小了。
lens[1]=1;
lens[2]=2;
lens[3]=3;
int max=0;
//从下至上递推
for(int i=4;i<=n;i++){
max=0;
//只循环i/2就可以了,因为后半段和前半段是等价的,相当于每次就只管剪两段
//而这俩段之中的一段大的之前已经记录了它的最大值了。
for(int j=1;j<=i/2;j++){
int len=lens[j]*lens[i-j];
if(max<len)max=len;
lens[i]=max;
}
}
max=lens[n];
return max;
}
}
2.贪婪算法
思路
当n>=5时,3(n-3)>n且3(n-3)>=2(n-2)且2(n-2)>n,所以当n>=5时,我们尽量将绳子剪成长度为3的段,其次剪成长度为2的段,那么还有一种情况n=4的时候呢,我们显然知道n=4时,应该剪为2跟2的段,最大的值为4。
代码
class Solution {
public int cuttingRope(int n) {
if(n <= 3) return n - 1;
int a = n / 3, b = n % 3;
//余0的话说明刚好全部剪成为3的段
if(b == 0) return (int)Math.pow(3, a);
//余1的话就是说明有个4,a--后就多整出一个4
if(b == 1) return (int)Math.pow(3, a - 1) * 4;
//余2的话也直接乘2
return (int)Math.pow(3, a) * 2;
}
}
15.二进制中1的个数
题目描述
请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
示例 1:
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 ‘1’。
示例 2:
输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 ‘1’。
示例 3:
输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 ‘1’。
思路
一个数与它减去1后的数做与运算,会把该整数最右边的1变成0
代码
public class Solution {
public int hammingWeight(int n) {
int res = 0;
while(n != 0) {
res++;
n &= n - 1;
}
return res;
}
}