练习题(2024/4/4)

本文介绍了如何使用动态规划算法解决字符串问题,包括计算回文子串的数量,将字符串进行Z字形变换,以及整数反转。同时涉及数据库查询技巧,如查找重复电子邮箱和从未订购的客户,以及查询部门工资最高的员工。
摘要由CSDN通过智能技术生成

1回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

动态规划思路:

  1. 确定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] 看上去都没啥关系。

所以我们要看回文串的性质。 如图:

下标01234
字符串scbabc
ii+1j-1j

                                                                                             

我们在判断字符串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 字形变换的基本思路:

  1. 创建 numRows 行的字符串数组,用于存储每一行的字符。
  2. 初始化一个变量表示当前行号,初始时为 0。
  3. 遍历输入字符串中的每个字符,按照 Z 字形的顺序将字符放入对应的行中。
    • 当前行号为 0 时,表示向下移动,将字符放入对应行;
    • 当当前行号为 numRows - 1 时,表示向上移动,将字符放入对应行。
  4. 遍历完成后,将每一行的字符连接起来,即为最终的 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

思路:

  1. 初始化一个变量 tmp为 0,用于存储反转后的整数。
  2. 循环操作,直到原始整数 x 为 0:
    • 每次取出 x 的最后一位数字 digit,可以通过 x % 10 实现。
    • 将 x 除以 10,即 x /= 10,去除掉最后一位数字。
    • 将 digit 添加到 tmp 的末尾,即 tmp= tmp* 10 + digit
  3. 在循环中,需要注意反转后的整数是否溢出的问题。可以通过判断 tmp与 INT_MAX 和 INT_MIN 的关系来检查是否溢出。
  4. 返回 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 出现了两次。

思路:

  1. 使用 SELECT 语句从电子邮箱表中选择需要检查重复的字段,例如 email 字段。
  2. 使用 GROUP BY 语句按照需要检查重复的字段进行分组,这里是按照 email 字段进行分组。
  3. 使用 HAVING 语句结合聚合函数(如 COUNT)来筛选出分组后的重复记录。
  4. 最后,通过执行 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

);

部门工资最高的员工

表: 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 在销售部的工资最高

思路:

  1. 首先,通过内联结合(INNER JOIN)将员工表(Employee)和部门表(Department)连接起来,使用部门ID(DepartmentId)进行连接。

  2. 在连接后的结果集中,使用窗口函数 RANK() 对每个部门内的员工按工资降序排列,并为他们分配排名。

  3. 将连接后的结果集作为子查询(或称为派生表),在外部查询中,从子查询中选择部门名(Department)、员工名(Employee)、工资(Salary)以及窗口函数生成的排名(rk)。

  4. 最后,外部查询中使用 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

  • 27
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值