《剑指 Offer》算法题(20/68)
本文最后更新于:1 年前
旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。
思路:
输入数组可以看成是两个递增数组的连接,其中第 2 个数组中的每一个值都不大于第 1 个数组中的任意一个值,且最小值是第 2 个数组的第 1 个值。
对于递增数组查询最小值,可以使用二分法查找。二分法涉及 3 个值,分别记为 a、b、c,分情况讨论——
(0)a > b > c,不存在;
(1)a < b < c,此时 a、b、c 都在后面数组,最小值是 a;
(2)a < b = c,此时 a、b、c 都在后面数组,最小值是 a;
(3)a = b < c,此时 a、b、c 都在后面数组,最小值是 a;
(4)a < b,b > c,此时 a、b 在前面数组,c 在后面数组,最小值在 b、c 之间,如果 b 是 c 的前一个元素,则最小值是 c;
(5)a = b,b > c,此时 a、b 在前面数组,c 在后面数组,最小值在 b、c 之间,如果 b 是 c 的前一个元素,则最小值是 c;
(6)a > b,b < c,此时 a 在前面数组,b、c 在后面数组,最小值在 a、b 之间,如果 a 是 b 的前一个元素,则最小值是 b;
(7)a > b,b = c,此时 a 在前面数组,b、c 在后面数组,最小值在 a、b 之间,如果 a 是 b 的前一个元素,则最小值是 b;
(8)a = b = c,如果 a、b、c 是连续的 3 个数,则最小值是 a;否则无法确定最小值所在区间,只能既查找 a、b 区间,也查找 b、c 区间,一旦在其中某个区间发现更小的值,就能够缩小查找范围。
实现:
1 |
|
1 |
|
时间复杂度:
空间复杂度:
矩阵中的路径
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。
思路:
回溯法。
遍历矩阵,如果矩阵中的某个值和字符串中的第一个值相同,则进入下一级,判断该值在周围四个方向的值和字符串中的下一个值是否相等,如果相等,则继续进入下一级,直到字符串中的值全部找完;否则,回到上一级。
实现:
1 |
|
1 |
|
时间复杂度:O(n)。
空间复杂度:O(n)。
扩展:打印两个字符间的一条最短路径
思路:
记录水平和竖直方向相对于起点位置的位移。
实现:
1 |
|
1 |
|
机器人的运动范围
地上有一个 m 行 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,它每次可以向左、右、上、下移动一格,但不能进入行坐标和列坐标的数位之和大于 k 的格子。例如,当 k 为 18 时,机器人能够进入方格 (35, 37),因为 3 + 5 + 3 + 7 = 18。但它不能进入方格 (35, 38),因为 3 + 5 + 3 + 8 = 19。请问该机器人能够达到多少个格子?
思路:
回溯法。
实现:
1 |
|
1 |
|
时间复杂度:
空间复杂度:
剪绳子
给你一根长度为 n 的绳子,请把绳子剪成 m 段(m、n 都是整数,n > 1 并且 m > 1),每段绳子的长度记为 k[0],k[1],…,k[m]。请问 k[0] x k[1] x … x k[m] 可能的最大乘积是多少?例如,当绳子的长度是 8 时,我们把它剪成长度分别为 2、3、3 的三段,此时得到的最大乘积是 18。
思路:
(1)动态规划法。
任何一根长度为 n 的绳子都可以分为两根长度之和为 n 的绳子,如果其中一根绳子长度为 x,则另一根绳子长度为 n - x。这样,问题就转变为了求两个绳子的问题了,这两根绳子可以再继续一分为二,直至绳子长度为 1,因为 1 分成两个小于 1 的数后,再相乘时值更小。
实现:
1 |
|
1 |
|
时间复杂度:O(n^2)。
空间复杂度:O(n)。
扩展:打印剪绳子的具体方案
实现:
1 |
|
1 |
|
(2)贪婪算法。
根据剪绳子的具体方案可以发现,长度为 n 的绳子,剪成的 m 段绳子,基本是由长度为 2、3 的绳子组成的,比较3 * (n - 3)
和2 * (n - 2)
,当n >= 5
时,3 * (n - 3) >= 2 * (n - 2)
,所以当长度大于或等于 5 时,应优先剪 3;否则,剪 2。
实现:
1 |
|
1 |
|
时间复杂度:O(1)。
空间复杂度:O(1)。
二进制中 1 的个数
请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
思路:
n & (n - 1)。
(1)当 n 的最右位是 1,n - 1 只是将最右位的 1 变为 0;
(2)当 n 的最右位是 0,n - 1 会先前找 1,找到后将该位置的 1 变为 0,该位置后面全部变为 1,然后变为了(1);
所以当 n != 0 时,n & (n - 1) 的操作会将最右边的 1 变为 0。
实现:
1 |
|
1 |
|
时间复杂度:O(n)。
空间复杂度:O(1)。
相关题目:用一条语句判断一个整数是不是 2 的整数次方
思路:
一个整数如果是 2 的整数次方,那么它的二进制表示中有且只有一位是 1,而其他所有位都是 0。
实现:
1 |
|
相关题目:计算两个数的二进制的不同位的个数
输入两个整数 m 和 n,计算需要改变 m 的二进制表示中的多少位才能得到 n。比如 10 的二进制表示为 1010,13 的二进制表示为 1101,需要改变 1010 中的 3 位才能得到 1101。
思路:
求两个数的异或,统计异或结果中 1 的位数。
实现:
1 |
|
1 |
|
数值的整数次方
实现函数 double Power(double base, int exponent),求 base 的 exponent 次方。不得使用库函数,同时不需要考虑大数问题。已知公式:
思路:
(1)当 base == 0 时,函数返回 0;
(2)当 exponent == 0 时,函数返回 1;
(3)当 exponent < 0 时,base = 1 / base,exponent = -exponent;
(4)利用提供的公式计算。
实现:
1 |
|
1 |
|
时间复杂度:O(logn)。
空间复杂度:O(logn)。
打印从 1 到最大的 n 位数
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
思路:
考虑大数问题,不能直接打印。
(1)输入数字 <= 0 时,不打印;
(2)输入数字 > 0 时,递归调用位数少 1 位的方法,直到调用到位数为 1 的方法后开始打印。
实现:
1 |
|
1 |
|
时间复杂度:
空间复杂度:O(n)。
扩展:实现任意两个整数的加(减)法
思路:
(1)两个正整数相加时,结果的位数最多比这两个数中最大值的位数多 1 位。按照小学加法,以递归方式依次从最低位开始加,是否有进位也需要带到下一次计算中;
(2)两个正整数相减时,需要先确定两个数中哪个更大,确保让大数减小数。结果的位数最多为这两个数中最大值的位数,计算以递归方式依次从最低位开始减,低位向高位借 1 时直接在高位减 1;
(2)当两个值中有负整数时,通过截取字符串变为正整数,同时转变为等价的运算。
实现:
1 |
|
1 |
|
删除链表的节点
题目一:在 O(1) 时间内删除链表节点
给定单向链表的头指针和一个节点指针,定义一个函数在 O(1) 时间内删除该节点。
思路:
(1)对于头节点,直接将头节点指向下一个节点;
(2)对于尾节点,从头遍历找到前一个节点,将该节点的下一个节点置为 null;
(3)对于中间的节点,将要删除的节点的信息修改成下一个节点的信息,本质是删除了下一个节点。
实现:
1 |
|
1 |
|
时间复杂度:O(1)。对于 n 个节点的链表,只有尾节点是 O(n),其余节点都是 O(1)。
空间复杂度:O(1)。
题目二:删除链表中重复的节点
在一个排序的链表中,如何删除重复的节点?
思路:
定义 3 个指针,分别指向已确定的节点(beforeNode),当前需要判断的节点(currentNode)及其下一个节点(afterNode)。为了防止头节点就是需要删除的重复节点,在头节点前构造一个不重复的节点,最后再将头节点指向该节点的下一个节点。
遍历节点,当 currentNode 和 afterNode 值不相等时,将 currentNode 放到 beforeNode 后,否则,记录这个值,再往后遍历,直到找到不等于这个值的节点,如果这个节点是尾节点,则直接将这个节点放到 beforeNode 后;否则将这个节点作为 currentNode,继续判断 currentNode 和 afterNode 值是否相等。
实现:
1 |
|
1 |
|
时间复杂度:
空间复杂度:
正则表达式匹配
请实现一个函数用来匹配包含’.‘和’*‘的正则表达式。模式中的字符’.‘表示任意一个字符,而’*'表示它前面的字符可以出现任意次(含 0 次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但与"aa.a"和“ab*a”均不匹配。
思路:
取模式串的前两个字符和字符串的一个字符——
(1)处理’*‘。如果模式串的第 2 个字符是’*‘,则从字符串的字符向后遍历,直到字符串的字符和模式串的第 1 个字符不相等,从这个位置开始后面的匹配;
(2)处理’.‘。因为’.'表示任意一个字符,所以直接从下一个字符开始匹配;
(3)判断模式串和字符串的一个字符是否相等。
实现:
1 |
|
1 |
|
时间复杂度:O(n)。
空间复杂度:O(n)。
表示数值的字符串
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100"、“5e2”、“-123”、“3.1416"及”-1E-16"都表示数值,但"12e"、“1a3.14”、“1.2.3”、"±5"及"12e+5.4"都不是。
思路:
‘e’、‘E’后面必须有数字,数字之间不能有’.';
字符可以是[0-9]、‘e’、‘E’;
‘e’或’E’之前的’.‘最多只能有一个;
‘+’、’-'开头时,只能有一个;
(例子中未完全体现限制,还需要根据常识补充)。
实现:
略。
时间复杂度:
空间复杂度: