11,机器人的运动范围
思路分析
这道题需要注意的是两个点,一个是n,m的范围,他们都是100及其以内的值。另外一个是他是从(0,0)开始的,然后以此向别的方法走。
上面的两个条件就确定了他不需要进行每个点的遍历,只能从初始点开始找,且我们也确定了判断能否进入格子的算法。
剩下的就很简单了,就是一个深度优先遍历,这个大家应该都会吧。
代码实现
/**
* @version v1.0
* @ProjectName: 数据结构
* @ClassName: Solution9
* @Description: 机器人的运动范围
* @Author: ming
* @Date: 2022/4/11 15:51
* 地上有一个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。请问该机器人能够到达多少个格子?
*
*/
public class Solution9 {
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n];
return dfs(0,0,m,n,k,visited);
}
private int dfs(int i, int j, int m, int n, int k, boolean[][] visited) {
//判断都有不成立的条件
if(i < 0 || i >= m || j < 0 || j >= n || (i%10 + i/10 + j%10 + j/10) > k
|| visited[i][j]) {
return 0;
}
//如果可以成立就将这个值改为true,表示已经访问过了
visited[i][j] = true;
return dfs(i + 1, j, m, n, k, visited) + dfs(i - 1, j, m, n, k, visited) +
dfs(i, j + 1, m, n, k, visited) + dfs(i, j - 1, m, n, k, visited) + 1;
}
}
12,I. 剪绳子
思路分析
方法一
动态规划:
首先要明确我们每次切绳子的长度都因该是大于等于2的,因为切1对于乘法来讲毫无意义。
最初进行数据的分析,当绳子为3的时候,结果为1 * 2 = 2。
当绳子为4的时候,第一次切2,剩余2,这个时候可以分情况:
- 如果不切的话,直接第一次切的长度2 * 剩余长度2
- 如果切的话,第一次切的长度2 * 剩余长度的最优切法2
- 之后在比较切和不切的大小,选最大的。
当绳子为5的时候
- 第一次切2,剩余3,这个时候可以分情况:
- 如果不切的话,直接第一次切的长度2 * 剩余长度3
- 如果切的话,第一次切的长度2 * 剩余长度的最优切法3
- 之后在比较切和不切的大小,选最大的,放入,
- 第二次切3,剩余长度为2,这个时候分情况讨论
- 如果不切的话,直接第一次切的长度3 * 剩余长度2
- 如果切的话,第一次切的长度3 * 剩余长度的最优切法2
- 之后在比较切和不切的大小,选最大的,再和之前切2的值进行比较,选择最大的放入。
之后以此类推
代码实现
/**
* @version v1.0
* @ProjectName: 数据结构
* @ClassName: Solution10
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/11 17:11
*/
public class Solution10 {
public int cuttingRope(int n) {
//创建一个存放最优解的数组,用于存放不同长度绳子的最优解
int[] p = new int[n + 1];
//我们已经知道2的最优解为2
p[2] = 1;
//i是指绳子的初始长度,j是指第一次切的长度
for (int i = 3; i <= n; i++) {
for (int j = 2; j < i; j++) {
//就是实现动态规划的步骤,先比较切完第一刀后直接乘以剩余长度和继续切这种情况谁的解比较大,
// 在和现在数组中这个位置的值进行比较选最大
p[i] = Math.max(p[i], Math.max(j * p[i-j], j * (i-j)));
}
}
return p[n];
}
}
方法二
找规律
当我们手动找了几个例子后,发现他分解的数字如果都是2或者3的时候,返回的是最大值。有因为绳子的长度是大于1的整数,所以之后的数字都可以用2和3表示,而且我们发现分出3的个数越多,最后的值就会越大。
例如12 可以将其分为6个2或者4个3,6个2的值为64,4个3的值为81
具体的思路在代码中解释
代码实现
/**
* @version v1.0
* @ProjectName: 数据结构
* @ClassName: Solution10
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/11 17:11
*/
public class Solution10 {
/**
* 也可以变成一个找规律的题,当我们手动找了几个例子后,发现他分解的数字如果都是2或者3的时候,返回的是最大值
* 有因为绳子的长度是大于1的整数,所以之后的数字都可以用2和3表示,而且我们发现分出3的个数越多,最后的值就会越大
* 例如12 可以将其分为6个2或者4个3,6个2的值为64,4个3的值为81
* @param n
* @return
*/
public static int cuttingRope2(int n) {
//直接输入小于等于3的值(之后的不符合这个规律)
if (n <= 3) {
return n - 1;
}
//有多少个3
int div = n / 3;
//全部取3之后余数
int rem = n % 3;
//结果
int res = 1;
//我们要的是有多少个3,并用于累加所以要循环
for (int i = 0; i < div; i++) {
//如果余数为0,就说明可以随便乘
if (rem == 0) {
res *= 3;
//如果余数有2的话,我们的3依旧可以随便乘,只是需要注意最后一次乘以3的时候需要在乘2
} else if (rem == 2) {
res *= 3;
if (i == div - 1) {
res *= 2;
}
//如果有余数为1的话,要将最后一个3分开,和1组合成两个2
}else if(rem == 1) {
if (i < div - 1) {
res *= 3;
}else if (i == div - 1) {
res = res * 2 * 2;
}
}
}
return res;
}
public static void main(String[] args) {
cuttingRope2(5);
}
}
13,二进制中1的个数
代码实现
/**
* @version v1.0
* @ProjectName: 数据结构
* @ClassName: Solution11
* @Description: 二进制中1的个数
* @Author: ming
* @Date: 2022/4/12 15:25
*
* 1011
* 101
* 10
* 1
*/
public class Solution11 {
/**
* 转换为二进制,我们只判断最后一位,如果是1的话就于2取余一定会有1,如果是1就count++,最后在让这个数向右移一位。
* 但是现实给了我当头一棒,他超时了。
* @param n
* @return
*/
public static int hammingWeight(int n) {
int m = 2;
int res = 0;
while(n != 0) {
if (n % m == 1) {
res += 1;
}
n = n >> 1;
}
return res;
}
/**
* 上面是通过取模来判断最后一位是啥,那么我们是否可以通过位运算直接进行查看最后一位是1的位置,然后在把他去掉呢
* 之前我写过一个找最右边1在哪个的一个表示式为A & (~A+1),我们可以对其进行修改,(~A+1)是最右边为1的位置不变,别的位置
* 都为0,这样子我们可以取反,变为最右边为1的位置为0别的位置都为1,然后在通过&运算,就变为了去掉最右边1的运算了
* @param n
* @return
*/
public static int hammingWeight2(int n) {
int res = 0;
while(n != 0) {
res++;
//将二进制的最右侧的1去掉。
n = n & (n - 1);
}
return res;
}
public static void main(String[] args) {
System.out.println(hammingWeight(11));
}
}
14,数值的整数次方
这道题是要用快速幂的。
快速幂
快速幂是一种能够对m的n次幂
快速进行计算的一种算法。能在log(n)
的时间内求出结果。
传统算法
一般情况下我们会想到直接对m
乘n
次不就得到了m的n次幂
了吗?也就是下面的这种算法,我们可以看到它的时间复杂度是O(n)
。
private static double power1(int a,int b){
double res = 1;
for (int i = 0; i < b ; i++){
res *= a;
}
return a == 0 ? 0 : res;
}
如果b足够大,很容易超时。
递归快速幂
递归快速幂的思想大致如下:
2^15
可以分解成求2^7 * 2^7 * 2
,2^7
分解为2^3 * 2^3 * 2
2^3
分解为2^1 * 2^1 * 2
,当幂次为1的时候就是递归的边界条件,我们直接return
。我们可以看到2^15
依赖2^7
的计算,2^7
依赖2^3
,
所以我们可以用递归来实现它
private static double powers(int a, int b) {
if(b == 1) {
return a;
}
if((b & 1) == 1) {
double temp = powers(a, b/2);
return temp * a * temp;
} else {
double temp = powers(a,b/2);
return temp * temp;
}
}
总结:
递归快速幂就是指将b不停的分割,从而进行简化操作
- 如果是奇数就变成
(a ^ (b/2)) * (a ^ (b/2)) * a
- 如果是偶数的话就变成了
(a ^ (b/2)) * (a ^ (b/2))
- 一直往后进行划分,直到b变成1为止。
非递归快速幂
非递归快速幂的思想大致如下:
我们通过递归快速幂可以看到不断除以2
,是不是跟右移动一位是一样的?所以我们换个角度,从n这个幂次
的二进制出发,例如2^42
,我们可以将42
化成101010
的二进制。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wVGrihEi-1649918801253)(%E5%89%91%E6%8C%87Offer.assets/1649757225596.png)]
上述式子我们是很容易知晓的,指数相加。可以看到我们把101010
拆成了100000
和1010
,这是因为我们在计算的过程中,会先计算m * m
== m^2
=m^10(2)
,然后再计算(m * m) * (m * m)
== m^4
m^100(2)
,m^8
m^1000(2)
不就是对指数在移位吗?有多少位二进制就计算几次。
所以像计算2^100000(2)
这种我们只需要移动100000(2)的长度6
就行了,每次将上次的结果*结果
得到新结果
,幂次就会 * 2
啦,我们知道* 2
是会后面补0
的,所以自然就慢慢得到100000
,那么新结果这个时候就是我们2^100000(2)
,首先2^1
* 2^1
变成了2^10(2)
,然后2^2
*2^2
变成了2^100(2)
,最后通过5次这样的计算
我们就可以算到2^100000(2)
。
回归正题,我们还有2^1010(2)
没求呢?2^1010(2)
分解成2^1000(2)
* 2^10(2)
,2^1000(2)
和2^10(2)
我们是不是在上述求2^100000(2)
的过程中会有计算到2^1000(2)
和2^10(2)
的步骤呢?所以我们只需要在计算到这的时候,记录一下当前的值就行了。根据我们的拆分规则,以为1
为分界点的,101010(2)
即100000(2) + 1000(2) + 10(2)
具体可以回归到上图观察下。每次我们遇到位数为1
的时候就需要记录一下,以便于贡献给下次为1
的时候计算。具体代码如下:
private static double power3(int a, int b) {
double res = 1;
double base = a;
whlie(b > 0) {
if((b & 1) == 1) {
//res是保存的数据,就比如上面说的,1010分为1000和10,我们最后要合并的,也就
//是1000 + 10,当我们第一次调用这个方法的时候就是在res中保存了一个10的结果
//当我再次调用的时候res = res * base 此时base是1000的结果,res是10的结果,*就是相加
res *= base;
}
base *= base;
b >>= 1;
}
}
当我们进行运算速度测试时,我们可以看到传统算法为11
,而其他两种为0
,在数据量特别大的时候,这种对比是特别明显的。传统的为O(n)
,快速幂为O(logn)
,所以为什么有这么大差别很明显了。
正片开始
这道题说了我们的n可能会存在负数,所以为了防止溢出,我们要给n的类型变为long类型,其余的和上面的一致。
public double myPow2(double x, int n) {
boolean is_minus = n < 0;
//这里需要注意越界问题
long i = n;
long m = Math.abs(i);
double res = 1;
while(m > 0) {
if ((m & 1) == 1) {
res *= x;
}
x *= x;
m >>= 1;
}
if (is_minus) {
res = 1 / res;
}
return res;
}
15,打印从1到最大的n位数
思路分析
这道题首先说明,有可能是我思路有问题,别人都是什么大数啥的,反正我看不懂,也想不到那方面。
我的思路就是我已经知道有几位数了,那么我们就创建一个相同大小的数组,之后遍历就行。
class Solution {
public static int[] printNumbers(int n) {
int bit = n;
int count = 0;
while(bit > 0) {
count = count * 10 + 9;
bit--;
}
int[] a = new int[count];
for (int i = 0; i < count; i++) {
a[i] = i + 1;
}
return a;
}
}
16,删除链表的节点
思路分析
链表题,我知道的就三种做法,一种是单指针,一种是双指针(比较麻烦的是指针的定制),一种是栈
代码实现
import java.util.Stack;
/**
* @version v1.0
* @ProjectName: 数据结构
* @ClassName: Solution14
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/12 21:39
*/
public class Solution14 {
/**
* 删除节点且返回删除后的头节点。
* 思路:
* 1,边界问题:头部和尾部的情况
* @param head
* @param val
* @return
*/
public ListNode deleteNode(ListNode head, int val) {
if (head == null) {
return null;
}
//头
while(head.val == val) {
return head.next;
}
ListNode pre = head;
//这里循环完后,就说明他的下一个值就是val
while (pre.next != null && pre.next.val != val) {
pre = pre.next;
}
//判断val所在的位置不是尾部且可以找到这个值
if (pre.next.next != null && pre.next != null) {
pre.next = pre.next.next;
} else {
pre.next = null;
}
return head;
}
/**
* 使用栈进行查找,只要不是val的就入栈,之后再倒出来
* @param head
* @param val
* @return
*/
public ListNode deleteNode2(ListNode head, int val) {
Stack<ListNode> stack = new Stack<>();
while (head != null) {
if (head.val != val) {
stack.push(head);
}
head = head.next;
}
//往出导的过程,先知道栈顶元素,之后让其的下一位为head(也就是末尾的下一位为head),然后在把head往前
//移动一位,也就是出栈,这样子重复下去,每个栈顶的元素的下一位就都变成了之前的出栈的前一位了。
while (!stack.isEmpty()) {
stack.peek().next = head;
head = stack.pop();
}
return head;
}
/**
* 双指针(快慢指针) 超时了。。。。。。。。。。
* 1,定制快慢指针,快指针的初始位置在慢指针后一个位置,之后就是同步操作
* @param head
* @param val
* @return
*/
public ListNode deleteNode3(ListNode head, int val) {
if (head == null) {
return head;
}
//头
while(head.val == val) {
return head.next;
}
//慢指针
ListNode node1 = head;
//快指针
ListNode node2 = head.next;
//
while (node1.next != null) {
if (node2.val == val) {
node1.next = node2.next;
} else {
node1 = node1.next;
node2 = node1.next;
}
}
return head;
}
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
}
17,正则表达式匹配
思路分析
代码实现
/**
* @version v1.0
* @ProjectName: 数据结构
* @ClassName: Solution15
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/13 16:03
*/
public class Solution15 {
public boolean isMatch(String s, String p) {
int sLen = s.length(),pLen = p.length();
//为了让s和p的第一个字符位于1的位置
boolean[][] memory = new boolean[sLen + 1][pLen + 1];
//如果两个字符都是null则直接匹配成功
memory[0][0] = true;
//从开头一直匹配到结尾
for (int i = 0; i < sLen; i++) {
//这里j必须从1开始,因为如果匹配字符串为null,那么一定匹配不成功
for (int j = 1; j < pLen; j++) {
//如果是'*'的需要分情况讨论总结一个固定的表达式
if (p.charAt(j -1) == '*') {
memory[i][j] = memory[i][j-2] || (i > 0
&& (s.charAt(i-1) == p.charAt(j-2) || p.charAt(j-2) == '.') && memory[i-1][j]);
}else {
memory[i][j] = i > 0 && (s.charAt(i-1) == p.charAt(j-1) || p.charAt(j-1) == '.')
&& memory[i-1][j-1];
}
}
}
return memory[sLen][pLen];
}
}
这道题挺难的,反正我做了两次都写不出来。。
18,表示数值的字符串
思路分析
我刚开始是想通过判断整数,小数,在之后按照数值进行一步步判断,但是写到一半,发现有很多的重复代码,而且换来换去,无非就是判断e,.,±,数值这几种情况。所以就像直接进行归纳即可。
* -,+只能用于开头或者e和E后面
* e和E只能有一个且前面要有一个数字
* .只能用于只能出现一次且在e前面,属于的情况都可以完成
代码实现
/**
* @version v1.0
* @ProjectName: 数据结构
* @ClassName: Solution16
* @Description: 表示数值的字符串
* @Author: ming
* @Date: 2022/4/13 16:32
* 归纳一下各个符号的作用范围
* -,+只能用于开头或者e和E后面
* e和E只能有一个且前面要有一个数字
* .只能用于只能出现一次且在e前面,属于的情况都可以完成
*/
public class Solution16 {
public boolean isNumber(String s) {
if (s == null || s.length() == 0) {
return false;
}
//去空格
s = s.trim();
//±号的判断
boolean symbolFlag = false;
//e的判断
boolean eFlag = false;
//.的判断
boolean spotFlag = false;
//判断数子的标记
boolean numFlag = false;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) >= '0' && s.charAt(i) <= '9') {
numFlag = true;
//这个我刚开始遇到了要给特例就是0e,为了解决这个特例,在e或者E出现后numFlag修改为false
} else if ((s.charAt(i) == 'e' || s.charAt(i) == 'E') && eFlag == false && numFlag == true) {
eFlag = true;
numFlag = false;
} else if ((s.charAt(i) == '.') && eFlag == false && spotFlag == false) {
spotFlag = true;
} else if ((s.charAt(i) == '+' || s.charAt(i) == '-') && (i == 0 || (s.charAt(i - 1) == 'e' || s.charAt(i - 1) == 'E'))) {
symbolFlag = true;
} else {
return false;
}
}
return numFlag;
}
}
19,调整数组顺序使奇数位于偶数前面
思路分析
可以用荷兰国旗问题来解决。
代码实现
/**
* @version v1.0
* @ProjectName: 数据结构
* @ClassName: Solution17
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/13 17:47
*/
public class Solution17 {
public int[] exchange(int[] nums) {
int l = 0;
int r = nums.length - 1;
int less = l - 1;
int more = nums.length - 1;
while (l < more) {
if (nums[1] % 2 != 0) {
swap(nums, l++, ++less);
}else {
swap(nums, l, more--);
}
}
return nums;
}
public static void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
20,链表中倒数第k个节点
思路分析
链表类的题目都可以通过双指针问题解决。而这道题要找的是倒数,我就想到了栈。
代码实现
import java.util.Stack;
/**
* @version v1.0
* @ProjectName: 数据结构
* @ClassName: Solution18
* @Description: 链表中倒数第k个节点
* @Author: ming
* @Date: 2022/4/14 14:26
*/
public class Solution18 {
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
/**
* 链表题的通用做法就是快慢指针,要让我们找倒数第k个位置的值,那么就让快指针先走k-1步,之后一起走,
* 当快指针走完时慢指针就是我们的值
* @param head
* @param k
* @return
*/
public ListNode getKthFromEnd(ListNode head, int k) {
int n = k - 1;
ListNode r = head;
ListNode l = head;
for (int i = 0; i < n; i++) {
r = r.next;
}
while (r.next != null) {
r = r.next;
l = l.next;
}
return l;
}
/**
* 求倒数的值,栈刚好是先进后出的
* @param head
* @param k
* @return
*/
public ListNode getKthFromEnd2(ListNode head, int k) {
Stack<ListNode> stack = new Stack<>();
ListNode l = head;
while (l.next != null) {
stack.add(l);
l = l.next;
}
for (int i = 0; i < k - 1; i++) {
l = stack.pop();
}
return l;
}
}