learn-tech/专栏/重学数据结构与算法-完/加餐课后练习题详解.md
2024-10-16 11:12:24 +08:00

293 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
加餐 课后练习题详解
专栏虽已结束,但学习不应停止。我看到很多同学依然还在这里学习,一部分同学积极地在留言区和大家分享学习总结和练习题答案。
我几乎在每个课时的结尾都留下了一道练习题,目的是帮助你检测和巩固本课时的重点内容,抑或是引出后续课时中的内容。在我处理留言的过程中,发现很多同学想要练习题详细解答过程以及答案,所以就有了今天的这一篇加餐内容,希望对你有所帮助。
接下来我会给出每个课时练习题的解题思路和答案,如果你没有找到对应的练习题答案,那么请你在正课中查找。
01 | 复杂度:如何衡量程序运行的效率?
【问题】 评估一下,如下的代码片段,时间复杂度是多少?
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
for (k = 0; k < n; k++) {
}
for (m = 0; m < n; m++) {
}
}
}
解析 在上面的代码中
35 行和 68 显然是一个 O(n) 复杂度的循环这两个循环是顺序结构因此合在一起的复杂度是 O(n) + O(n) = O(2n) = O(n)。
29 行是一个 for 循环它的时间复杂度是 O(n)。这个 for 循环内部嵌套了 O(n) 复杂度的代码因此合在一起就是 O(n ² ) 的时间复杂度
在代码的最外部 110 行又是一个 O(n) 复杂度的循环内部嵌套了 O(n ² ) 的时间复杂度的代码因此合在一起就是 O(n ³ ) 的时间复杂度
02 | 数据结构昂贵的时间复杂度转换成廉价的空间复杂度
问题 在下面这段代码中如果要降低代码的执行时间 4 行代码需要做哪些改动呢如果做出改动后是否降低了时间复杂度呢
public void s2_2() {
int count = 0;
for (int i = 0; i <= (100 / 7); i++) {
for (int j = 0; j <= (100 / 3); j++) {
if ((100-i*7-j*3 >= 0)&&((100-i*7-j*3) % 2 == 0)) {
count += 1;
}
}
}
System.out.println(count);
}
【解析】 代码的第 4 行可以改为:
for (int j = 0; j <= (100-7*i) / 3; j++) {
代码改造完成后,时间复杂度并没有变小。但由于减少了一些不必要的计算量,程序的执行时间变少了。
03 | 增删查:掌握数据处理的基本操作,以不变应万变
【问题】 对于一个包含 5 个元素的数组,如果要把这个数组元素的顺序翻转过来。你可以试着分析该过程需要对数据进行哪些操作?
【解析】 假设原数组 a = {1,2,3,4,5},现在要更改为 a = {5,4,3,2,1}。要想得到新的数组,就要找到 “1” 和 “5”再分别把它们赋值给对方。因此这里主要会产生大量的基于索引位置的查找动作。
04 | 如何完成线性表结构下的增删查?
【问题】 给定一个包含 n 个元素的链表,现在要求每 k 个节点一组进行翻转,打印翻转后的链表结果。例如,链表为 1 -> 2 -> 3 -> 4 -> 5 -> 6k = 3则打印 321654。
【解析】 我们给出一些提示。利用链表翻转的算法,这个问题应该很简单。利用 3 个指针prev、curr、next执行链表翻转每次得到了 k 个翻转的结点就执行打印。
05 | 栈:后进先出的线性表,如何实现增删查?
【问题】 给定一个包含 n 个元素的链表,现在要求每 k 个节点一组进行翻转,打印翻转后的链表结果。例如,链表为 1 -> 2 -> 3 -> 4 -> 5 -> 6k = 3则打印 321654。要求用栈来实现。
【解析】 我们用栈来实现它,就很简单了。你可以牢牢记住,只要涉及翻转动作的题目,都是使用栈来解决的强烈信号。
具体的操作如下,设置一个栈,不断将队列数据入栈,并且实时记录栈的大小。当栈的大小达到 k 的时候,全部出栈。我们给出核心代码:
while (tmp != null && count < k) {
stack.push(tmp.value);
tmp = tmp.next;
count++;
}
while (!stack.isEmpty()) {
System.out.print(stack.pop());
}
07 | 数组如何实现基于索引的查找
详细分析和答案请翻阅 18 课时例题 1
08 | 字符串如何正确回答面试中高频考察的字符串匹配算法
详细分析和解题步骤请参考 17 课时例题 1
10 | 哈希表如何利用好高效率查找的利器”?
详细分析和答案请翻阅 15 课时例题 1
11 | 递归如何利用递归求解汉诺塔问题
详细分析和答案请翻阅 16 课时例题 1
12 | 分治如何利用分治法完成数据查找
问题 在一个有序数组中查找出第一个大于 9 的数字假设一定存在例如arr = { -1, 3, 3, 7, 10, 14, 14 }则返回 10
解析 在这里提醒一下带查找的目标数字具备这样的性质
第一它比 9
第二它前面的数字除非它是第一个数字 9
因此当我们作出向左走或向右走的决策时必须满足这两个条件代码如下
public static void main(String[] args) {
int targetNumb = 9;
// 目标有序数组
int[] arr = { -1, 3, 3, 7, 10, 14, 14 };
int middle = 0;
int low = 0;
int high = arr.length - 1;
while (low <= high) {
middle = (high + low) / 2;
if (arr[middle] > targetNumb && (middle == 0 || arr[middle - 1] <= targetNumb)) {
System.out.println("第一个比 " + targetNumb + " 大的数字是 " + arr[middle]);
break;
} else if (arr[middle] > targetNumb) {
// 说明该数在low~middle之间
high = middle - 1;
} else {
// 说明该数在middle~high之间
low = middle + 1;
}
}
}
14 | 动态规划:如何通过最优子结构,完成复杂问题求解?
详细分析和答案,请翻阅 16 课时例题 3。
15 | 定位问题才能更好地解决问题:开发前的复杂度分析与技术选型
【问题】 下面的代码采用了两个 for 循环去实现 two sums。那么能否只使用一个 for 循环完成呢?
private static int[] twoSum(int[] arr, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < arr.length; i++) {
map.put(arr[i], i);
}
for (int i = 0; i < arr.length; i++) {
int complement = target - arr[i];
if (map.containsKey(complement) && map.get(complement) != i) {
return new int[] { map.get(complement), i };
}
}
return null;
}
解析 原代码中 3 和第 6 行的 for 循环合并后就需要把 map 的新增查找合在一起执行则代码如下
private static int[] twoSum(int[] arr, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < arr.length; i++) {
int complement = target - arr[i];
if (map.containsKey(complement) && map.get(complement) != i) {
return new int[] { map.get(complement), i };
}
else{
map.put(arr[i], i);
}
}
return null;
}
16 | 真题案例算法思维训练
问题 如果现在是个线上实时交互的系统客户端输入 x服务端返回斐波那契数列中的第 x 那么这个问题使用上面的解法是否可行
解析 这里给你一个小提示既然我这么问答案显然是不可行的如果不可行原因是什么呢我们又该如何解决
注意题目中给出的是一个实时系统当用户提交了 x如果在几秒内没有得到系统响应用户就会卸载 App
一个实时系统必须想方设法在 O(1) 时间复杂度内返回结果因此一个可行的方式是在系统上线之前把输入 x 1100 的结果预先就计算完并且保存在数组里当收到 1100 范围内输入时O(1) 时间内就可以返回如果不在这个范围则需要计算计算之后的结果返回给用户并在数组中进行保存以方便后续同样输入时能在 O(1) 时间内可以返回
17 | 真题案例数据结构训练
问题 对于树的层次遍历我们再拓展一下如果要打印的不是层次而是蛇形遍历又该如何实现呢蛇形遍历就是 s 形遍历即奇数层从左到右偶数层从右到左
解析 这里要对数据的顺序进行逆序处理直观上你需要立马想到栈毕竟只有栈是后进先出的结构是能快速实现逆序的具体而言需要建立两个栈 s1 s2进栈的顺序是s1 先右后左s2 先左后右两个栈交替出栈的结果就是 s 形遍历代码如下
public ArrayList<ArrayList<Integer>> Print(TreeNodes pRoot) {
// 先右后左
Stack<TreeNodes> s1 = new Stack<TreeNodes>();
// 先左后右
Stack<TreeNodes> s2 = new Stack<TreeNodes>();
ArrayList<ArrayList<Integer>> list = new ArrayList<ArrayList<Integer>>();
list.add(pRoot.val);
s1.push(pRoot);
while (s1.isEmpty() || s2.isEmpty()) {
if (s1.isEmpty() && s2.isEmpty()) {
break;
}
if (s2.isEmpty()) {
while (!s1.isEmpty()) {
if (s1.peek().right != null) {
list.add(s1.peek().right.val);
s2.push(s1.peek().right);
}
if (s1.peek().left != null) {
list.add(s1.peek().left.val);
s2.push(s1.peek().left);
}
s1.pop();
}
} else {
while (!s2.isEmpty()) {
if (s2.peek().left != null) {
list.add(s2.peek().left.val);
s1.push(s2.peek().left);
}
if (s2.peek().right != null) {
list.add(s2.peek().right.val);
s1.push(s2.peek().right);
}
s2.pop();
}
}
}
return list;
}
18 | 真题案例(三): 力扣真题训练
【问题】 给定一个链表,删除链表的倒数第 n 个节点。例如,给定一个链表: 1 -> 2 -> 3 -> 4 -> 5, 和 n = 2。当删除了倒数第二个节点后链表变为 1 -> 2 -> 3 -> 5。你可以假设给定的 n 是有效的。额外要求就是,要在一趟扫描中实现,即时间复杂度是 O(n)。这里给你一个提示,可以采用快慢指针的方法。
【解析】 定义快慢指针slow 和 fast 并同时指向 header。然后让 fast 指针先走 n 步。接着让二者保持同样的速度一起往前走。最后fast 指针先到达终点,并指向了 null。此时slow 指针的结果就是倒数第 n 个结点。比较简单,我们就不给代码了。
19 | 真题案例(四):大厂真题实战演练
【问题】 小明从小就喜欢数学,喜欢在笔记里记录很多表达式。他觉得现在的表达式写法很麻烦,为了提高运算符优先级,不得不添加很多括号。如果不小心漏了一个右括号的话,就差之毫厘,谬之千里了。因此他改用前缀表达式,例如把 (2 + 3) * 4写成* + 2 3 4这样就能避免使用括号了。这样的表达式虽然书写简单但计算却不够直观。请你写一个程序帮他计算这些前缀表达式。
【解析】 在这个题目中,输入就是前缀表达式,输出就是计算的结果。你可以假设除法为整除,即 “5/3 = 1”。例如输入字符串为 + 2 3输出 5输入字符串为 * + 2 2 3输出为 12输入字符串为 * 2 + 2 3输出为 10。
假设输入字符串为 * 2 + 2 3即 2*(2+3)。第一个字符为运算符号 *,它将对两个数字进行乘法。如果后面紧接着的字符不全是数字字符,那就需要暂存下来,先计算后面的算式。一旦后面的计算完成,就需要接着从后往前去继续计算。因为从后往前是一种逆序动作,我们能够很自然地想到可以用栈的数据结构进行存储。代码如下:
public static void main(String[] args) {
Stack<Object> stack = new Stack<Object>();
String s = "* + 2 2 3";
String attr[] = s.split(" ");
for (int i = attr.length - 1; i >= 0; i--) {
if (!(attr[i].equals("+") || attr[i].equals("-") || attr[i].equals("*") || attr[i].equals("/"))) {
stack.push(Integer.parseInt(attr[i]));
} else {
int a = (int) stack.pop();// 出栈
int b = (int) stack.pop();// 出栈
int result = Cal(a, b, attr[i]); // 调用函数计算结果值
stack.push(result); // 结果进栈
}
}
int ans = (int) stack.pop();
System.out.print(ans);
}
public static int Cal(int a, int b, String s) {
switch (s) {
case "+":
return a + b;
case "-":
return a - b;
case "*":
return a * b;
case "/":
return a / b;
}
return 0;
}
以上这些练习题你做得怎么样呢?还能回忆起来每道题是源自哪个算法知识点或哪个课时吗?
你可以把课后习题和课程中的案例都当作一个小项目,自己动手实践,即使有些题目你还不能写出完整的代码,那也可以尝试写出解题思路,从看不明白到能够理解,再到能联想到用什么数据结构和算法去解决什么样的问题,这是一个循序渐进的过程,切勿着急。
通过留言可以看出,你们都在认真地学习这门课程,也正因如此,我才愿意付出更多的时间优化这个已经完结的专栏。所以,请你不要犹豫,尽管畅所欲言,在留言区留下你的思考,也欢迎你积极地提问,更欢迎你为专栏提出建议,这样我才能更直接地看到你们的想法和收获。也许你的一条留言,就是下一篇加餐的主题。