刷题_打家劫舍

打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

1
2
3
4
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

1
2
3
4
5
输入:[2,7,9,3,1]
输出:12
解释:
偷窃1号房屋(金额=2),偷窃3号房屋(金额=9),接着偷窃5号房屋(金额=1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

1
2
1 <= nums.length <= 100
0 <= nums[i] <= 400

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int rob(vector<int>& nums) {
vector<int> ar;
ar.reserve(nums.size()+1);
ar[0] = 0;
ar[1] = nums[0];
int max = ar[1];
for(int i = 2; i<=nums.size(); ++i)
{
ar[i] = std::max(ar[i-2]+nums[i-1],ar[i-1]);
if(ar[i]>max)max = ar[i];
}
return max;
}
};

打家劫舍2

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例 1:

1
2
3
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

1
2
3
4
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:

1
2
输入:nums = [1,2,3]
输出:3

提示:

1
2
1 <= nums.length <= 100
0 <= nums[i] <= 1000

题解

这道题是打家劫舍的进阶,和初级版的不同之处是,这道题中的房屋是首尾相连的,第一间房屋和最后一间房屋相邻,因此第一间房屋和最后一间房屋不能在同一晚上偷窃。

和其相似,这道题也可以使用动态规划解决。建议首先了解动态规划的思想。


首先考虑最简单的情况。如果只有一间房屋,则偷窃该房屋,可以偷窃到最高总金额。如果只有两间房屋,则由于两间房屋相邻,不能同时偷窃,只能偷窃其中的一间房屋,因此选择其中金额较高的房屋进行偷窃,可以偷窃到最高总金额。

注意到当房屋数量不超过两间时,最多只能偷窃一间房屋,因此不需要考虑首尾相连的问题。如果房屋数量大于两间,就必须考虑首尾相连的问题,第一间房屋和最后一间房屋不能同时偷窃。

如何才能保证第一间房屋和最后一间房屋不同时偷窃呢?如果偷窃了第一间房屋,则不能偷窃最后一间房屋,因此偷窃房屋的范围是第一间房屋到最后第二间房屋;如果偷窃了最后一间房屋,则不能偷窃第一间房屋,因此偷窃房屋的范围是第二间房屋到最后一间房屋。

假设数组numsnums的长度为nn。如果不偷窃最后一间房屋,则偷窃房屋的下标范围是[0,n2][0, n-2];如果不偷窃第一间房屋,则偷窃房屋的下标范围是[1,n1][1,n−1]。在确定偷窃房屋的下标范围之后,即可用第1题的方法解决。对于两段下标范围分别计算可以偷窃到的最高总金额,其中的最大值即为在nn间房屋中可以偷窃到的最高总金额。

假设偷窃房屋的下标范围是[start,end][start,end],用dp[i]dp[i]表示在下标范围[start,i][start,i]内可以偷窃到的最高总金额,那么就有如下的状态转移方程:

dp[i]=max(dp[i2]+nums[i],dp[i1])dp[i]=max(dp[i−2]+nums[i],dp[i−1])

边界条件为:

{dp[start]=nums[start]只有一间房屋,则偷窃该房屋dp[start+1]=max(nums[start],nums[start+1])只有两间房屋,偷窃其中金额较高的房屋\begin{cases} \textit{dp}[\textit{start}] = \textit{nums}[\textit{start}] & 只有一间房屋,则偷窃该房屋 \\ \textit{dp}[\textit{start}+1] = \max(\textit{nums}[\textit{start}], \textit{nums}[\textit{start}+1]) & 只有两间房屋,偷窃其中金额较高的房屋 \end{cases}

计算得到dp[end]dp[end]即为下标范围[start,end][start,end]内可以偷窃到的最高总金额。

分别取(start,end)=(0,n2)(start,end)=(0,n−2)(start,end)=(1,n1)(start,end)=(1,n−1)进行计算,取两个dp[end]dp[end]中的最大值,即可得到最终结果。

根据上述思路,可以得到时间复杂度O(n)O(n)和空间复杂度O(n)O(n)的实现。

考虑到每间房屋的最高总金额只和该房屋的前两间房屋的最高总金额相关,因此可以使用滚动数组,在每个时刻只需要存储前两间房屋的最高总金额,将空间复杂度降到O(1)O(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int robRange(vector<int>& nums, int start, int end) {
int first = nums[start], second = max(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; i++) {
int temp = second;
second = max(first + nums[i], second);
first = temp;
}
return second;
}

int rob(vector<int>& nums) {
int length = nums.size();
if (length == 1) {
return nums[0];
} else if (length == 2) {
return max(nums[0], nums[1]);
}
return max(robRange(nums, 0, length - 2), robRange(nums, 1, length - 1));
}
};

打家劫舍3

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为root

除了root之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

给定二叉树的root。返回在不触动警报的情况下,小偷能够盗取的最高金额 。

示例 1:

image-20220518184546102

1
2
3
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

示例 2:

image-20220518184621752

1
2
3
输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9

提示:

  • 树的节点数在 [1, 104] 范围内
  • 0 <= Node.val <= 104

题解

思路与算法

简化一下这个问题:一棵二叉树,树上的每个点都有对应的权值,每个点有两种状态(选中和不选中),问在不能同时选中有父子关系的点的情况下,能选中的点的最大权值和是多少。

我们可以用f(o)f(o)表示选择oo节点的情况下,oo节点的子树上被选择的节点的最大权值和;g(o)g(o)表示不选择oo节点的情况下,oo节点的子树上被选择的节点的最大权值和;llrr代表oo的左右孩子。

  • oo被选中时,oo的左右孩子都不能被选中,故oo被选中情况下子树上被选中点的最大权值和为llrr不被选中的最大权值和相加,即f(o)=g(l)+g(r)f(o)=g(l)+g(r)
  • oo不被选中时,oo的左右孩子可以被选中,也可以不被选中。对于oo的某个具体的孩子xx,它对oo的贡献是xx被选中和不被选中情况下权值和的较大值。故g(o)=max{f(l),g(l)}+max{f(r),g(r)}g(o)=max\{f(l),g(l)\}+max\{f(r),g(r)\}

至此,我们可以用哈希表来存ffgg的函数值,用深度优先搜索的办法后序遍历这棵二叉树,我们就可以得到每一个节点的ffgg。根节点的ffgg的最大值就是我们要找的答案。

我们不难给出这样的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
unordered_map <TreeNode*, int> f, g;

void dfs(TreeNode* node) {
if (!node) {
return;
}
dfs(node->left);
dfs(node->right);
f[node] = node->val + g[node->left] + g[node->right];
g[node] = max(f[node->left], g[node->left]) + max(f[node->right], g[node->right]);
}

int rob(TreeNode* root) {
dfs(root);
return max(f[root], g[root]);
}
};

假设二叉树的节点个数为nn

我们可以看出,以上的算法对二叉树做了一次后序遍历,时间复杂度是O(n)O(n);由于递归会使用到栈空间,空间代价是O(n)O(n),哈希表的空间代价也是O(n)O(n),故空间复杂度也是O(n)O(n)

我们可以做一个小小的优化,我们发现无论是f(o)f(o)还是g(o)g(o),他们最终的值只和f(l)f(l)g(l)g(l)f(r)f(r)g(r)g(r)有关,所以对于每个节点,我们只关心它的孩子节点们的ffgg是多少。我们可以设计一个结构,表示某个节点的ffgg值,在每次递归返回的时候,都把这个点对应的ffgg返回给上一级调用,这样可以省去哈希表的空间。

代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct SubtreeStatus {
int selected;
int notSelected;
};

class Solution {
public:
SubtreeStatus dfs(TreeNode* node) {
if (!node) {
return {0, 0};
}
auto l = dfs(node->left);
auto r = dfs(node->right);
int selected = node->val + l.notSelected + r.notSelected;
int notSelected = max(l.selected, l.notSelected) + max(r.selected, r.notSelected);
return {selected, notSelected};
}

int rob(TreeNode* root) {
auto rootStatus = dfs(root);
return max(rootStatus.selected, rootStatus.notSelected);
}
};

复杂度分析

时间复杂度:O(n)O(n)。上文中已分析。

空间复杂度:O(n)O(n)。虽然优化过的版本省去了哈希表的空间,但是栈空间的使用代价依旧是O(n)O(n),故空间复杂度不变。

MySQL_SQL

内容

  1. SQL(Structure Query Language, 结构化查询语言)
  2. SQL划分为3个类别,DD(Definition)L、DM(Manipulation)L、DC(Control)L
  3. 库操作
  4. 表操作
  5. CRUD
  6. SQL语句和索引的关系

SQL

SQL即Structure Query Language,结构化查询语言。它是关系型数据库的通用语言。

SQL可划分为以下三种类别:

  1. DDL(Data Definition Language)
    • 数据定义语言,与数据库、表、列、索引等数据库对象的定义有关。
    • 常用的语句关键字主要包括createdropalter等。
  2. DML(Data Manipulation Language)
    • 数据操纵语句,用于添加、删除、更新和查询数据库记录,并检查数据完整性。
    • 常用的语句关键字主要包括insertdeleteupdateselect等。
  3. DCL(Data Control Language)
    • 数据控制语句,用于控制不同的许可和访问级别的语句。与数据库、表、字段、用户的访问权限和安全级别有关。
    • 主要的语句关键字包括grantrevoke

库操作

查询数据库:show databases;

创建数据库:create database dbname;

删除数据库:drop database dbname;

选择数据库:use dbname;

我们下面以学校的数据库为例,创建库表。

1
2
CREATE DATABASE school;
USE school;

表操作

表的设计要素:

  1. 字段的数据类型
  2. 完整性约束条件的规范
  3. 一对一/一对多/多对多实体关系对应的表设计原则
  • 创建表
1
2
3
4
5
6
CREATE TABLE user(
id INT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT,
name VARCHAR(50) UNIQUE NOT NULL,
age TINTINT NOT NULL,
sex ENUM('M', 'W') NOT NULL
)ENGINE=INNODB DEFAULT CHARSET=utf8;

对于存储引擎以及默认字符集的配置文件,
在windows下为mysql安装目录下的my.ini
linux下为/etc/my.cnf
配置字段为[mysqld]

  • 查看表
1
show tables;

image-20220517103024114

  • 查看表的描述信息
1
2
DESC user;
describe user; # 全拼

image-20220517103053213

  • 表已创建后,查看表的创建语句
1
2
SHOW CREATE TABLE user;
SHOW CREATE TABLE user\G # \G结尾和;结尾的区别在于显示方式不同

image-20220517103908450

  • 删除表 - drop (不光是表的内容,整个表的结构都没了)
1
DROP TABLE user;

不常用的操作

View(视图)

在数据库中,左边栏,存在一个view,可以存放一个表中的多个字段的查询结果,也可以存放多个表连接之后的临时结果。

实际用途中,视图可以用于屏蔽与实际数据库的差异,比如数据库的实际字段可能不是用户请求的字段,视图起了个别名。
也可以用于隐藏一些细节。把实际数据库中的内容提取出来的不敏感的部分放到视图里,建立虚拟的字段,供用户使用。

存储过程

类似于批处理脚本。可以没有返回值。

函数

封装操作过程,可以定义变量,可以有控制语句、循环语句等。必须有返回值。

触发器

触发器的作用:比如插入、删除一条记录等等操作,可以进行相应的回调。
触发器是基于行的,即基于记录的。当操作很多行时,不建议使用触发器。

总结

存储过程和函数一般是专门操作数据库的后端人员负责的。实际上,存储过程和函数的操作,完全可以在上层服务用高级语言(C++等)完成。数据库可以只负责简单的计算。

CRUD

Create - 插入

1
2
INSERT INTO user VALUES(1, 'zhangsan', 20, 'M'); #如果不指明插入的属性,则必须按照字段的顺序全部填入
INSERT INTO user(name,age,sex) VALUES('lisi', 22, 'W'); #指明插入的属性,由于id设置为自增性,所以不用填写。
  • 每次添加一个
1
2
3
4
5
INSERT INTO user(name,age,sex) VALUES('zhangsan', 20, 'M');
INSERT INTO user(name,age,sex) VALUES('gaoyang', 22, 'W');
INSERT INTO user(name,age,sex) VALUES('chenwei', 20, 'M');
INSERT INTO user(name,age,sex) VALUES('zhangfan', 21, 'W');
INSERT INTO user(name,age,sex) VALUES('zhanglan', 21, 'W');
  • 批量增加
1
2
3
4
5
6
INSERT INTO user(name,age,sex) VALUES
('zhangsan', 20, 'M'),
('gaoyang', 22, 'W'),
('chenwei', 20, 'M'),
('zhangfan', 21, 'W'),
('zhanglan', 21, 'W');

最终表中存放的数据是一致的。有啥区别?

我们使用sql,相当于mysql server的一个client。

每次添加一个:

  1. client和server进行tcp三次握手,建立通信链路;
  2. client发送sql语句到server上接收并处理,返回处理结果;
  3. server和client断开连接,tcp四次挥手,释放通信链路。

把每次添加一条记录的操作抽象为3次步骤。则总共需要3*n个步骤。

而如果是批量添加:

  1. client和server进行tcp三次握手,建立通信链路;
  2. client发送sql语句到server上接收并处理,返回处理结果;
  3. server和client断开连接,tcp四次挥手,释放通信链路。

则总共仅需3个步骤,即可全部插入。

Delete - 删除

1
2
DELETE FROM user;				#删除表中所有数据
DELETE FROM user WHERE id=1; #只删除id为1的记录

Update - 更新

1
UPDATE user SET age=age+1 WHERE name='gaoyang';	#调正gaoyang的年龄+1

Retrieve - 查询

在这里我们示例一些简单的查询语句。

  • 无条件查询全部字段
1
SELECT * FROM user;	#不推荐,最好还是老老实实地写出所有字段名称,因为表的结构之后可能会变更,可能会影响业务代码。
  • 条件查询指定字段
1
2
3
SELECT name,age,sex FROM user WHERE age>=21;
SELECT name,age,sex FROM user WHERE age>20 AND age<22;
SELECT name,age,sex FROM user WHERE BETWEEN 20 AND 22;#闭区间
1
2
3
# 以下两个sql语句效果一致。
SELECT name,age,sex FROM user WHERE age=20 OR age=21;
SELECT name,age,sex FROM user WHERE age IN (20,21);
  • 使用通配符查询
1
SELECT name,age,sex FROM user WHERE name LIKE "zhang%";#必须用LIKE,不能用=等号,否则会把%通配符看作实际字符。
  • 判断为空不用=,而用IS NULL
1
2
SELECT name,age,sex FROM user WHERE name IS NULL;
SELECT name,age,sex FROM user WHERE name IS NOT NULL;

去重

有两种去重的方式

  1. distinct - 简单地去重
  2. group by - 功能强大,耗时
1
SELECT DISTINCT age FROM user;

image-20220517114906800

合并查询 - union

格式如下

1
2
3
4
5
6
SELECT expression1, expression2, ..., expression
FROM table1 [WHERE conditions]
UNION [ALL | DISTINCT(default)]
SELECT expression1, expression2, ..., expression
FROM table2 [WHERE conditions];
# union默认为distinct去重,all表示显示重复的记录项
1
2
3
4
5
SELECT name,age,sex
FROM user WHERE age>=21
UNION ALL
SELECT name,age,sex
FROM user WHERE sex='M';

image-20220517163204447

SQL语句和索引的关系

1
SELECT name,age,sex FROM user WHERE age=20 OR age=21;

通常都说带逻辑与的SQL语句会用到索引,带逻辑或的SQL语句不会用到索引。比如上面这句带逻辑或的SQL语句不会用到索引吗?错误。

如果仅仅是对于这个SQL语句的表象来说,是不会用到索引的。但是MySQL实际运行过程中是会对用户提交的SQL语句进行优化的。

OR的语义是:要么..., 要么...。对于逻辑或语句,MySQL有可能将其转化为union合并查询,即前后分别执行两个不同的不带逻辑或的SQL语句,这样的SQL语句是完全可以用不到索引的。

类似地,带in的SQL语句,表面上是能用到索引;带not in的SQL语句,表面上用不到索引。带仅仅限于MySQL不做优化的情况。

所以,会不会用到索引不能只看表面的SQL语句,而要看MySQL实际如何优化。

连接查询

表1:Employees

EmployeeID Name DepartmentID
1 张三 101
2 李四 102
3 王五 102
4 赵六 103
​5​ ​钱七​ ​NULL​

表2:Departments

DepartmentID DepartmentName
101 人事部
102 技术部
104 财务部

笛卡尔积(交叉查询)

1
2
3
4
5
6
7
8
9
SELECT
e.EmployeeID AS EmpID,
e.Name AS EmpName,
e.DepartmentID AS A_DeptID,
d.DepartmentID AS B_DeptID,
d.DepartmentName AS DeptName
FROM Employees e -- 给表起别名 'e'
CROSS JOIN Departments d; -- 给表起别名 'd'
-- 没有 ON 子句!
EmpID EmpName A_DeptID B_DeptID DeptName
​​1​​ 张三 101 ​​101​​ 人事部
​​1​​ 张三 101 ​​102​​ 技术部
​​1​​ 张三 101 ​​104​​ 财务部
​​2​​ 李四 102 ​​101​​ 人事部
​​2​​ 李四 102 ​​102​​ 技术部
​​2​​ 李四 102 ​​104​​ 财务部
​​3​​ 王五 102 ​​101​​ 人事部
​​3​​ 王五 102 ​​102​​ 技术部
​​3​​ 王五 102 ​​104​​ 财务部
​​4​​ 赵六 103 ​​101​​ 人事部
​​4​​ 赵六 103 ​​102​​ 技术部
​​4​​ 赵六 103 ​​104​​ 财务部
5 钱七 NULL 101 人事部
5 钱七 NULL 102 技术部
5 钱七 NULL 104 财务部

内连接 (INNER JOIN / JOIN)

通俗解释:只保留“两头都能配上对”的数据。​​ 两边都有记录的才显示出来。

1
2
3
4
SELECT Employees.EmployeeID, Employees.Name, Departments.DepartmentName
FROM Employees
INNER JOIN Departments
ON Employees.DepartmentID = Departments.DepartmentID;

结果:

EmployeeID Name DepartmentName
1 张三 人事部
2 李四 技术部
3 王五 技术部
  • 赵六(员工ID4) 的部门ID是103,而部门表里没有103号部门,所以被​​过滤掉​​了。
  • 财务部(部门ID 104) 没有任何员工,也​​没出现​​。

左连接 (LEFT JOIN / LEFT OUTER JOIN)

通俗解释:“左边表全保,右边表尽量配”。​​ 左表所有记录都显示,右表有匹配的也显示出来,​​没匹配的补空值(NULL)​​。

场景:​​ 列出​​所有员工​​(不管有没有部门),​​同时​​如果员工有部门,就显示部门名称(没有部门的员工,部门名称显示为空)。

1
2
3
4
SELECT Employees.EmployeeID, Employees.Name, Departments.DepartmentName
FROM Employees
LEFT JOIN Departments
ON Employees.DepartmentID = Departments.DepartmentID;

结果:

EmployeeID Name DepartmentName DepartmentID
1 张三 人事部 101
2 李四 技术部 102
3 王五 技术部 102
4 赵六 ​NULL​ 103
5 钱七 NULL NULL
  • 左边员工表 (Employees) 的 5 条记录​​全部保留​​。
  • 张三李四王五 在部门表里找到了匹配 (101102),所以显示部门名称。
  • 赵六(部门ID 103) 在部门表里没找到对应的名字,所以部门名称显示为 ​NULL​。
  • 钱七(员工ID 5)的部门 ID 本来就是 NULL,所以无法查询,显示 NULL。
  • 部门表中孤立的 财务部(104) ​​没出现​​。

如何筛选出A表中外键为NULL的记录


如此查询,便能筛选出钱七。
这个图的意思是:在外面的红色部分,与B没有交集,意思就是说,外面这部分没有记录来自B的外键信息,与B无关联。

右连接 (RIGHT JOIN / RIGHT OUTER JOIN)

(其实相当于左连接的镜像,左连接时,A和B表交换书写位置就是他们的右连接)

通俗解释:“右边表全保,左边表尽量配”。​​ 右表所有记录都显示,左表有匹配的也显示出来,​​没匹配的补空值(NULL)​​。​​(和左连接方向相反)​

​场景:​​ 列出​​所有部门​​(不管有没有员工),​​同时​​如果部门下有员工,就列出员工姓名;没人的部门,员工信息显示为空。

1
2
3
4
SELECT Employees.EmployeeID, Employees.Name, Departments.DepartmentName
FROM Employees
RIGHT JOIN Departments
ON Employees.DepartmentID = Departments.DepartmentID;

结果:

EmployeeID Name DepartmentName
1 张三 人事部(101)
2 李四 技术部(102)
3 王五 技术部(102)
​NULL​ ​NULL​ 财务部(104)
  • 右边部门表 (Departments) 的 3 条记录(101,102,104)​​全部保留​​。
  • 人事部(101)、技术部(102) 找到了员工 (张三 / 李四王五),显示姓名。
  • 财务部(104) 在员工表里没任何员工,所以员工ID和姓名都是 ​NULL​。
  • 员工 赵六 (部门ID 103)、钱七(员工ID 5,部门ID NULL) ​​没出现​​(因为它的部门103在部门表里不存在,不属于右表保留范围)。

全连接 (FULL JOIN / FULL OUTER JOIN)

通俗解释:“两个表我都全要!管你配不配得上!”​​ 两个表的所有记录都保留。能配上对的就合并成一行显示。配不上对的,各自那边的空缺就用 ​NULL​ 填充。

​场景:​​ 想做一个​​完整的组织结构/花名册快照​​,既要看到所有员工(包括没部门的),也要看到所有部门(包括没员工的)。信息齐全!

1
2
3
4
SELECT Employees.EmployeeID, Employees.Name, Departments.DepartmentName
FROM Employees
FULL JOIN Departments
ON Employees.DepartmentID = Departments.DepartmentID;

结果:

EmployeeID Name DepartmentName DepartmentID
1 张三 人事部 101
2 李四 技术部 102
3 王五 技术部 102
4 赵六 ​NULL​ 103
5 钱七 NULL NULL
​NULL​ ​NULL​ 财务部 104
  • ​全部 6 种情况都展示出来了:​
    1. 有员工有部门 (张三-人事部李四-技术部王五-技术部)。
    2. 有员工​​没​​(匹配的)部门 (赵六钱七)。
    3. 有部门​​没​​(匹配的)员工 (财务部)。

如何筛选出A表或B表的外键为NULL的记录

如此查询,便能筛选出钱七。

由于本例中B表没有外键,因此无法体现B表中没有外键的记录。

连接查询(Join)和联合查询(Union)的区别

​联合查询(Union Query):​​ 这个概念容易和连接混淆。

  • ​通俗解释:​​ 把两张结构_相似_的表 ​​上下拼起来​​。就像把《番茄供应商名单》和《鸡蛋供应商名单》摞在一起,变成《食材供应商总名单》。注意:是垂直叠加!
  • ​核心要求:​
    • 两张表查询结果的​​列数必须一样​​。
    • 对应列的数据类型要​​兼容​​(比如数字对数字,字符串对字符串)。
1
2
3
SELECT EmployeeID, Name FROM Employees -- 查员工ID和名字
UNION -- 或者 UNION ALL (包含重复行)
SELECT DepartmentID, DepartmentName FROM Departments; -- 查部门ID和名字
EmployeeID/DepartmentID Name/DepartmentName
1 张三
2 李四
3 王五
4 赵六
5 钱七
101 人事部
102 技术部
104 财务部
注意:这个例子不常用,只是为了展示UNION效果。UNION更常用于合并同类型数据,比如不同年份的销售记录表

SELECT ... FROM A, B WHERE ...SELECT ... FROM A JOIN B ON ...的区别

这两者​​功能上对于内连接(INNER JOIN)是等效的​​,但存在关键区别:​​语法标准、可读性、维护性以及错误预防能力​​。

特性 SELECT ... FROM A, B WHERE ... (隐式连接) SELECT ... FROM A JOIN B ON ... (显式连接)
​语法本质​ 老式SQL标准,FROM子句用逗号分隔表,WHERE定义条件和连接 新式SQL-92标准,用JOIN关键字明确指定连接类型和条件
​连接类型​ 默认是​​交叉连接(CROSS JOIN)​​,需要WHERE过滤才能变成内连接 ​明确指出连接类型​​ (INNER/LEFT/RIGHT/FULL JOIN)
​连接条件位置​ 和过滤条件​​混在WHERE子句中​ 连接条件在​​独立的ON子句​​,过滤条件在WHERE(逻辑分离)
​可读性 & 维护性​ 差(复杂查询时条件混乱) 优(清晰区分连接条件和过滤逻辑)
​防止笛卡尔积错误​ 弱(忘记WHERE会导致全组合) 强(JOIN必须配ON或USING,否则语法错误)
​支持外连接​ 部分数据库不支持(如Oracle老语法用(+) ​所有数据库都支持​​标准外连接写法(LEFT/RIGHT/FULL JOIN)
​现代推荐度​ ⚠️ 不推荐(尤其复杂查询) ✅ ​​强烈推荐​

练习

查询平均成绩大于60分的同学的学号和平均成绩。

1
2
3
4
select s_id, avg(sc_score)
from score
group by s_id
having avg(sc_score)>60

查询所有同学的学号、姓名、选课总数、总成绩。

1
select s_id, s_name, count(c_id), sum(sc_score) from student left join score on student.s_id = score.s_id;

也可以这么写,嵌套select:

1
2
3
4
5
select student.s_id, student.s_name, count_id as XKS, sum_id as ZCJ
from student
left join
(select s_id, count(c_id), as count_id, sum(sc_score) as sum_id from score group by s_id) as ss
on student.s_id = ss.s_id;

查询没有选全部课的同学的学号、姓名。

1
2
3
4
select student.s_id, student.s_name from student, score
where student.s_id = score.s_id
group by student.s_id
having count(score.c_id) < (select count(c_id) from course);

先让score表补充一列学生的姓名。
再让此临时表,按学号(确定1人)分组,每一组的s_id数就是这个人选的课程数。

结果

删除Kim老师课的Score表记录
为了看到效果,临时增加一条成绩。

1
2
3
delete from score
where c_id in
(select course.c_id from course inner join teacher on course.t_id = teacher.t_id where teacher.t_name = 'Kim');

后面的select,先让course表补充一列老师的名字,之后筛选出老师名字是Kim的course;
in用于批量处理。

结果: