问题一:机器人的运动范围
题目描述
地上有一个$m$行和$n$列的方格,横纵坐标范围分别是 $0∼m−1$和$0∼n−1$。
一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格。
但是不能进入行坐标和列坐标的数位之和大于$k$的格子。
请问该机器人能够达到多少个格子?
样例1
1 | 输入:k=7, m=4, n=5 |
样例2
1 | 输入:k=18, m=40, n=40 |
题目分析
(BFS) $O(nm)$
这是一个典型的宽度优先搜索问题,我们从 (0, 0) 点开始,每次朝上下左右四个方向扩展新的节点即可。
扩展时需要注意新的节点需要满足如下条件:
之前没有遍历过,这个可以用个$bool$数组来判断;
- 没有走出边界;
- 横纵坐标的各位数字之和小于$k$;
- 最后答案就是所有遍历过的合法的节点个数。
时间复杂度
每个节点最多只会入队一次,所以时间复杂度不会超过方格中的节点个数。
最坏情况下会遍历方格中的所有点,所以时间复杂度就是$O(nm)$。
C++代码
1 | class Solution{ |
总结
广度优先遍历思想:首先访问起始顶点v,然后由v出发,依次访问v的各个未访问的邻接顶点$w_1,w_2,…,w_i$,然后再依次访问$w_1,w_2,…,w_i$ 的所有的未被访问过的邻接顶点;再从这些访问过的顶点出发,再访问它们所有未被访问过的邻接顶点……依次类推,直到图中所有的顶点已经访问过。以这个题目为例,在每一步中,它的下一步访问节点是从这一步节点的上下左右四个方向选出一个位置来进行访问,然后基于这一步的下一步,再访问下一步的的下一步,因此是属于广度优先遍历的方法。
广度优先遍历是通过队列实现的,也就是先入先出,先来先服务
深度优先遍历思想:首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任一顶点$w_1$ ,再访问与$w_1$邻接且未被访问的任一顶点$w_2$,……重复上述过程。当不能再继续向下访问时1,依次退回到最近被访问过的顶点,若它还有未被访问过的顶点,则从该顶点开始重复上述的过程。week1里面的寻找数组中的路径就是利用的这种思想(主要就是在考虑当前步的时候要加上下一步的递归来进行判断)。
深度优先遍历是通过栈来实现的,也就是先进后出,先来后服务
问题二:剪绳子
题目描述
给你一根长度为$n$绳子,请把绳子剪成 $m$段($m$、$n$都是整数,$2≤n≤58$ 并且 $m≥2$)。
每段的绳子的长度记为k[0]、k[1]、……、k[m]。k[0]k[1] … k[m] 可能的最大乘积是多少?
例如当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到最大的乘积18。
样例
1 | 输入:8 |
题目分析
(数学) $O(n)$
这道题目是数学中一个很经典的问题。
下面我们给出证明:
首先把一个正整数$N$拆分成若干正整数只有有限种拆法,所以存在最大乘积。
假设 $N=n1+n2+…+nk$,并且$n1×n2×…×nk$是最大乘积。
显然1不会出现在其中;
如果对于某$i$有$n_i≥5$,那么把$n_i$拆分成 $3+(n_i−3)$,我们有 $3(n_i−3)=3n_i−9>ni$;
如果$n_i=4$,拆成 $2+2$乘积不变,所以不妨假设没有4;
如果有三个以上的2,那么$3×3>2×2×2$,所以替换成3乘积更大;
综上,选用尽量多的3,直到剩下2或者4时,用2。
时间复杂度分析
当$n$比较大时,$n$会被拆分成 $⌈n/3⌉$个数,我们需要计算这么多次减法和乘法,所以时间复杂度是$O(n)$。
C++代码
1 | class Solution{ |
总结
这个题目也可以用动态规划的思想解决
动态规划的问题具有以下几个特点:
- 问题是求解一个最优解
- 原始问题可以划分为许多的子问题,子问题中还具有相同的子问题
- 原始问题的最优解可以有子问题的最优解得到
在这个问题中,可以划分为许多的子问题,比如$f(10)=f(4)*f(6)$ ,那么我们在分析问题的时候需要从上至下,在求解问题的时候需要从下至上。代码如下
1 | class Solution(){ |
问题三:二进制中1的个数
题目描述
输入一个32位整数,输出该数二进制表示中1的个数。
注意:
- 负数在计算机中用其绝对值的补码来表示。
样例1
1 | 输入:9 |
样例2
1 | 输入:-2 |
题目分析
把一个整数 (无论正负或0)减去1,再和原整数做与运算,会把该整数最右边的一个1变为0,例如:110100减1后变为110011,二者进行与操作后,得到110000,最后边的1变为了0,而前面的位都不变。
这样,我们可以利用这这一结论来从左向右依次将整数的最右边的1变为0,当该整数的所有位为1的位均变为0之后,便统计到了该整数二进制中1的个数。
C++代码
1 | class Solution{ |
总结
这个题目就是巧妙的利用了二进制中位运算进行分析。
变形的题目还有就是求两个整数m和n经过多少次二进制变换才能将m变换为n,这个就是先将m和n做异或操作,然后再将异或操作的结果作为二进制,计算出这个二进制中1的个数就可以。
问题四:数值的整数次方
题目描述
实现函数double Power(double base, int exponent),求base的exponent次方。
不得使用库函数,同时不需要考虑大数问题。
注意:
- 不会出现底数和指数同为0的情况
样例1
1 | 输入:10 ,2 |
样例2
1 | 输入:10 ,-2 |
题目分析
这个题目的分析在week1中的斐波那契数中见过,就是使用快速幂的思想
时间复杂度分析
$O(log n)$
C++代码
1 | class Solution{ |
总结
这个题目重点在于对时间复杂度的考虑,常规的计算方法就是$O(n)$ 的时间复杂度。
常规的做法如下:
1 | class Solution { |
问题五:在O(1)内的时间删除链表节点
题目分析
给定单向链表的一个节点指针,定义一个函数在O(1)时间删除该结点。
假设链表一定存在,并且该节点一定不是尾节点。
样例
1 | 输入:链表 1->4->6->8 |
题目分析
这个题目就是在考虑时间复杂度下面下功夫,很显然常规的解法都是$O(n)$,废话不说,这个题目很简单
C++代码
1 | /** |
总结
其实吧,这个题目的条件很友善,说明删除的节点不是最后一节点,如果没有这个条件的话这个题目就会很复杂
1 | void deleteNode(ListNode **pHead, ListNode* pDelNode) { |
问题六:删除链表中数值重复的节点
题目描述
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留。
样例1
1 | 输入:1->2->3->3->4->4->5 |
样例2
输入:1->1->1->2->3
输出:2->3
题目分析
(线性扫描) $O(n)$
为了方便处理边界情况,我们定义一个虚拟元素$dummy$指向链表头节点。
然后从前往后扫描整个链表,每次扫描元素相同的一段,如果这段中的元素个数多于1个,则将整段元素直接删除。
时间复杂度
整个链表只扫描一遍,所以时间复杂度是$O(n)$。
C++代码
1 | class Solution { |
问题七:正则表达式匹配
题目描述
请实现一个函数用来匹配包括'.'
和'*'
的正则表达式。
模式中的字符'.'
表示任意一个字符,而'*'
表示它前面的字符可以出现任意次(含0次)。
在本题中,匹配是指字符串的所有字符匹配整个模式。
例如,字符串"aaa"
与模式"a.a"
和"ab*ac*a"
匹配,但是与"aa.a"
和"ab*a"
均不匹配。
样例
输入:
s=”aa”
p=”a*”
输出:true
题目分析
这个题目是一个典型的动态规划问题。
转态转移:$f[i][j]$表示$s[i,…]$和$p[j,…]$相匹配
状态转移:
- p[j]是正常字符,$f[i][j]$=s[i]==p[j]&&$f[i+1][j+1]$;
- p[j]是”.”,$f[i][j]$=$f[i+1][j+1]$
- p[j+1]是”*”,$f[i][j]=f[i+1][j]||f[i][j+2]$
C++代码
1 | class Solution { |
总结
这个题目在运用动态规划的时候要主要要使用一个数组记录已经访问过的元素,这里就是用到了数组f
问题八:表示数值的字符串
问题描述
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。
例如,字符串"+100"
,"5e2"
,"-123"
,"3.1416"
和"-1E-16"
都表示数值。
但是"12e"
,"1a3.14"
,"1.2.3"
,"+-5"
和"12e+4.3"
都不是。
注意:
- 小数可以没有整数部分,例如.123等于0.123;
- 小数点后面可以没有数字,例如233.等于233.0;
- 小数点前面和后面可以有数字,例如233.666;
- 当e或E前面没有数字时,整个字符串不能表示数字,例如.e1、e1;
- 当e或E后面没有整数时,整个字符串不能表示数字,例如12e、12e+5.4;
样例:
1 | 输入: "0" |
题目分析
(模拟,字符串处理) $O(n)$
先去除行首和行尾空格;
行首如果有一个正负号,直接忽略;
如果字符串为空或只有一个’.’,则不是一个合法数;
循环整个字符串,去掉以下几种情况:
- ‘.’或’e’多于1个;
- ‘.’在’e’后面出现;
- ‘e’后面或前面为空,或者’e’前面紧跟着’.’;
- ‘e’后面紧跟着正负号,但正负号后面为空;
剩下的情况都合法;
时间复杂度分析
整个字符串只遍历一遍,所以时间复杂度是 $O(n)$。
C++ 代码
1 | class Solution { |
总结
就是考察自己细不细心,要学会进行调试代码
问题九:调整数组顺序使奇数位于偶数前面
题目描述
输入一个整数数组,实现一个函数来调整该数组中数字的顺序。
使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分。
样例
1 | 输入:[1,2,3,4,5] |
题目分析
(双指针扫描) $O(n)$
用两个指针分别从首尾开始,往中间扫描。扫描时保证第一个指针前面的数都是奇数,第二个指针后面的数都是偶数。
每次迭代时需要进行的操作:
- 第一个指针一直往后走,直到遇到第一个偶数为止;
- 第二个指针一直往前走,直到遇到第一个奇数为止;
- 交换两个指针指向的位置上的数,再进入下一层迭代,直到两个指针相遇为止;
时间复杂度
当两个指针相遇时,走过的总路程长度是 $n$,所以时间复杂度是 $O(n)$。
C++代码
1 | class Solution { |
总结
这个题目就是容易在l和r的取值上面犯错,不过可以在调试中解决,也算比较简单的一个题目。
问题十:找出链表中倒数第k各节点
问题描述
输入一个单链表,输出此链表中的倒数第 K 个节点。(去除头结点,节点计数从 1 开始)。
解法一:两次遍历法
解题思想
(1)遍历单链表,遍历同时得出链表长度 N 。
(2)再次从头遍历,访问至第 N - K 个节点为所求节点。
图解过程
C++代码
1 | //计算链表长度 |
解法二:双指针法
解题思想
(1)定义两个指针 p1 和 p2 分别指向链表头节点。
(2)p1 前进K 个节点,则 p1 与 p2 相距 K 个节点。
(3)p1,p2 同时前进,每次前进 1 个节点。
(4)当 p1 指向到达链表末尾,由于 p1 与 p2 相距 K 个节点,则 p2 指向目标节点。
图解过程
C++代码
1 | ListNode* findKthTail(ListNode *pHead, int K){ |
问题十一:链表中存在环的问题
快慢指针法(满指针走一步,快指针走两步)
总结
- 如果不存在环的话,那么两个指针只能在链表末尾相遇;如果存在环的话,如果两个指针相遇且相遇的节点的狭义节点不是空节点的话,那么链表中肯定是有环的
- 在定义环的入口的时候,只要分别定义两个指针,一个指针是从相遇的节点出发,一个是从头结点出发,这两个节点相遇的节点就是换的入口
- 计算环的长度的时候,慢指针和快指针从相遇的节点出发,还是按照原来的规则进行遍历,当这两个指着再次相遇的时候慢指针走过的路径长度也就是换的长度
判断链表是否有环
解题思想
- 定义两个指针分别为 slow,fast,并且将指针均指向链表头节点。
- 规定,slow 指针每次前进 1 个节点,fast 指针每次前进两个节点。
- 当 slow 与 fast 相等,且二者均不为空,则链表存在环。
1 | bool isExistLoop(ListNode* pHead) { |
定位环入口
1 | //找到环中的相遇节点 |
计算环的长度
1 | int getLoopLength(ListNode* head){ |
总结
休息了,近期不熬夜了。。。。。。