1回文子串
给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:s = "abc" 输出:3 解释:三个回文子串: "a", "b", "c"
示例 2:
输入:s = "aaa" 输出:6 解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
动态规划思路:
- 确定dp数组(dp table)以及下标的含义
本题尝试使用动态规划,难点是dp数组怎么定义,首先肯定想就是一个一维的字符数组(字符串)肯定会用vector dp来定义啊,为什么不行呢?dp[i]代表什么呢?代表[0……i]字符串有dp[i]个回文子串?那么它的递推公式怎么整?dp[i]和dp[i-1]和dp[i+1]有关系么?没有明显的关系,所以dp数组无法这样定义。
dp[i] 和 dp[i-1] ,dp[i + 1] 看上去都没啥关系。
所以我们要看回文串的性质。 如图:
下标 | 0 | 1 | 2 | 3 | 4 |
字符串s | c | b | a | b | c |
i | i+1 | j-1 | j |
我们在判断字符串S是否是回文,那么如果我们知道 s[1],s[2],s[3] 这个子串是回文的,那么只需要比较 s[0]和s[4]这两个元素是否相同,如果相同的话,这个字符串s 就是回文串。
那么此时我们是不是能找到一种递归关系,也就是判断一个子字符串(字符串的下表范围[i,j])是否回文,依赖于,子字符串(下表范围[i + 1, j - 1])) 是否是回文。
所以为了明确这种递归关系,我们的dp数组是要定义成一位二维dp数组。布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
2 确定递推公式
在确定递推公式时,就要分析如下几种情况。
整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。
当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。
当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况
- 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
- 情况二:下标i 与 j相差为1,例如aa,也是回文子串
- 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
3 dp数组如何初始化
dp[i][j]可以初始化为true么? 当然不行,怎能刚开始就全都匹配上了。
所以dp[i][j]初始化为false。
4 确定遍历顺序
遍历顺序可有有点讲究了。
首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。
dp[i + 1][j - 1] 在 dp[i][j]的左下角,如图:
dp[ i ][ j-1] | dp[ i ] [ j ] |
dp[ i+1 ][ j-1 ] | dp[i+1][ j ] |
如果这矩阵是从上到下,从左到右遍历,那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。
所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。
代码如下:
class Solution {
public:
int countSubstrings(string s) {
// 创建一个二维动态规划数组,用于记录回文子串的情况
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
int result = 0; // 用于记录回文子串的数量
// 反向遍历字符串 s
for (int i = s.size() - 1; i >= 0; i--) { // 注意遍历顺序
// 正向遍历字符串 s
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j]) { // 如果当前字符相同
if (j - i <= 1) { // 情况一:长度为1的子串或长度为2的子串都是回文子串
result++; // 回文子串数量加一
dp[i][j] = true; // 标记该子串是回文子串
} else if (dp[i + 1][j - 1]) { // 情况二:如果内部子串是回文子串,则整体子串也是回文子串
result++; // 回文子串数量加一
dp[i][j] = true; // 标记该子串是回文子串
}
}
}
}
return result; // 返回回文子串的数量
}
};
2. Z 字形变换
将一个给定字符串 s
根据给定的行数 numRows
,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 "PAYPALISHIRING"
行数为 3
时,排列如下:
P A H N A P L S I I G Y I R
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"
。
请你实现这个将字符串进行指定行数变换的函数:
string convert(string s, int numRows);
示例 1:
输入:s = "PAYPALISHIRING", numRows = 3 输出:"PAHNAPLSIIGYIR"
示例 2:
输入:s = "PAYPALISHIRING", numRows = 4 输出:"PINALSIGYAHRPI" 解释: P I N A L S I G Y A H R P I
示例 3:
输入:s = "A", numRows = 1 输出:"A"
思路
Z 字形变换的思路可以通过模拟人工排列字符的方式来实现。以下是实现 Z 字形变换的基本思路:
- 创建 numRows 行的字符串数组,用于存储每一行的字符。
- 初始化一个变量表示当前行号,初始时为 0。
- 遍历输入字符串中的每个字符,按照 Z 字形的顺序将字符放入对应的行中。
- 当前行号为 0 时,表示向下移动,将字符放入对应行;
- 当当前行号为 numRows - 1 时,表示向上移动,将字符放入对应行。
- 遍历完成后,将每一行的字符连接起来,即为最终的 Z 字形变换后的字符串。
通过以上步骤,即可实现 Z 字形变换。
代码如下:
class Solution {
public:
string convert(string s, int numRows) {
// 如果行数小于2,则直接返回原字符串
if (numRows < 2) return s;
// 创建 numRows 个字符串,用于存储每一行的字符
vector<string> rows(numRows);
// 初始化方向变量和当前行号变量
bool down = false; // 初始方向为向上
int row = 0; // 当前行号从0开始
// 遍历字符串 s 中的每个字符
for (int i = 0; i < s.length(); i++) {
// 将当前字符添加到对应行的字符串中
rows[row] += s[i];
// 判断是否需要改变方向
if (row == 0 || row == numRows - 1) // 达到 一行的头部或者尾部 为转折点
down = !down; // 方向取反
row += down ? 1 : -1; // 根据方向更新当前行号
}
// 将每一行的字符串连接起来,形成最终的结果字符串
string ans = "";
for (int i = 0; i < numRows; i++)
ans += rows[i];
return ans; // 返回结果字符串
}
};
3 整数反转
给你一个 32 位的有符号整数 x
,返回将 x
中的数字部分反转后的结果。
如果反转后整数超过 32 位的有符号整数的范围 [−231, 231 − 1]
,就返回 0。
假设环境不允许存储 64 位整数(有符号或无符号)。
示例 1:
输入:x = 123 输出:321
示例 2:
输入:x = -123 输出:-321
示例 3:
输入:x = 120 输出:21
示例 4:
输入:x = 0 输出:0
思路:
- 初始化一个变量 tmp为 0,用于存储反转后的整数。
- 循环操作,直到原始整数
x
为 0:- 每次取出
x
的最后一位数字digit
,可以通过x % 10
实现。 - 将
x
除以 10,即x /= 10
,去除掉最后一位数字。 - 将
digit
添加到 tmp 的末尾,即 tmp= tmp* 10 + digit
。
- 每次取出
- 在循环中,需要注意反转后的整数是否溢出的问题。可以通过判断 tmp与
INT_MAX
和INT_MIN
的关系来检查是否溢出。 - 返回 tmp。
代码如下:
class Solution {
public:
int reverse(int x) {
int tmp = 0; // 用于存储反转后的整数
while (x != 0) { // 当 x 不为 0 时执行循环
// 检查反转后的整数是否溢出 INT 类型的范围
if (tmp < INT_MIN / 10 || tmp > INT_MAX / 10) {
return 0; // 如果溢出,则返回 0
}
int digit = x % 10; // 获取 x 的最后一位数字
x /= 10; // 将 x 的最后一位数字去掉
tmp = tmp * 10 + digit; // 将获取的数字添加到 tmp 的末尾
}
return tmp; // 返回反转后的整数
}
};
4查找重复的电子邮箱
表: Person
+-------------+---------+ | Column Name | Type | +-------------+---------+ | id | int | | email | varchar | +-------------+---------+ id 是该表的主键(具有唯一值的列)。 此表的每一行都包含一封电子邮件。电子邮件不包含大写字母。
编写解决方案来报告所有重复的电子邮件。 请注意,可以保证电子邮件字段不为 NULL。
以 任意顺序 返回结果表。
结果格式如下例。
示例 1:
输入: Person 表: +----+---------+ | id | email | +----+---------+ | 1 | a@b.com | | 2 | c@d.com | | 3 | a@b.com | +----+---------+ 输出: +---------+ | Email | +---------+ | a@b.com | +---------+ 解释: a@b.com 出现了两次。
思路:
- 使用
SELECT
语句从电子邮箱表中选择需要检查重复的字段,例如email
字段。 - 使用
GROUP BY
语句按照需要检查重复的字段进行分组,这里是按照email
字段进行分组。 - 使用
HAVING
语句结合聚合函数(如COUNT
)来筛选出分组后的重复记录。 - 最后,通过执行 SQL 查询语句,可以得到重复的电子邮箱记录。
代码:
select Email
from Person
group by Email
having count(Email) > 1;
5从不订购的客户
Customers
表:
+-------------+---------+ | Column Name | Type | +-------------+---------+ | id | int | | name | varchar | +-------------+---------+ 在 SQL 中,id 是该表的主键。 该表的每一行都表示客户的 ID 和名称。
Orders
表:
+-------------+------+ | Column Name | Type | +-------------+------+ | id | int | | customerId | int | +-------------+------+ 在 SQL 中,id 是该表的主键。 customerId 是 Customers 表中 ID 的外键( Pandas 中的连接键)。 该表的每一行都表示订单的 ID 和订购该订单的客户的 ID。
找出所有从不点任何东西的顾客。
以 任意顺序 返回结果表。
结果格式如下所示。
示例 1:
输入: Customers table: +----+-------+ | id | name | +----+-------+ | 1 | Joe | | 2 | Henry | | 3 | Sam | | 4 | Max | +----+-------+ Orders table: +----+------------+ | id | customerId | +----+------------+ | 1 | 3 | | 2 | 1 | +----+------------+ 输出: +-----------+ | Customers | +-----------+ | Henry | | Max | +-----------+
思路:
判断客户是否曾经下过订单的条件是:如果一个客户 ID 在 orders 表中不存在,这就意味着他们从未下过订单。
使用子查询:可以先查询出所有订购过的客户ID,然后通过主查询排除这些客户,从而得到从未订购的客户。
代码:
select customers.name as 'Customers'#
from customers
where customers.id not in
(
select customerid from orders
);
6 部门工资最高的员工
表: Employee
+--------------+---------+ | 列名 | 类型 | +--------------+---------+ | id | int | | name | varchar | | salary | int | | departmentId | int | +--------------+---------+ 在 SQL 中,id是此表的主键。 departmentId 是 Department 表中 id 的外键(在 Pandas 中称为 join key)。 此表的每一行都表示员工的 id、姓名和工资。它还包含他们所在部门的 id。
表: Department
+-------------+---------+ | 列名 | 类型 | +-------------+---------+ | id | int | | name | varchar | +-------------+---------+ 在 SQL 中,id 是此表的主键列。 此表的每一行都表示一个部门的 id 及其名称。
查找出每个部门中薪资最高的员工。
按 任意顺序 返回结果表。
查询结果格式如下例所示。
示例 1:
输入: Employee 表: +----+-------+--------+--------------+ | id | name | salary | departmentId | +----+-------+--------+--------------+ | 1 | Joe | 70000 | 1 | | 2 | Jim | 90000 | 1 | | 3 | Henry | 80000 | 2 | | 4 | Sam | 60000 | 2 | | 5 | Max | 90000 | 1 | +----+-------+--------+--------------+ Department 表: +----+-------+ | id | name | +----+-------+ | 1 | IT | | 2 | Sales | +----+-------+ 输出: +------------+----------+--------+ | Department | Employee | Salary | +------------+----------+--------+ | IT | Jim | 90000 | | Sales | Henry | 80000 | | IT | Max | 90000 | +------------+----------+--------+ 解释:Max 和 Jim 在 IT 部门的工资都是最高的,Henry 在销售部的工资最高
思路:
-
首先,通过内联结合(INNER JOIN)将员工表(Employee)和部门表(Department)连接起来,使用部门ID(DepartmentId)进行连接。
-
在连接后的结果集中,使用窗口函数
RANK()
对每个部门内的员工按工资降序排列,并为他们分配排名。 -
将连接后的结果集作为子查询(或称为派生表),在外部查询中,从子查询中选择部门名(Department)、员工名(Employee)、工资(Salary)以及窗口函数生成的排名(rk)。
-
最后,外部查询中使用
WHERE
子句,筛选出排名为1的记录,即每个部门工资最高的员工。
代码:
#选择部门名,员工名,工资
select Department,Employee,Salary
from (
#使用窗口函数为每个部门的员工排名
select d.name as Department ,e.name as Employee,e.salary as Salary,
rank() over(partition by DepartmentId order by salary desc) as rk
from Employee as e
join Department as d
on e.DepartmentId=d.id
) as t
#筛选出每个部门员工工资最高的
where t.rk=1