diff --git "a/categories/csp\345\210\267\351\242\230\350\256\260\345\275\225/index.xml" "b/categories/csp\345\210\267\351\242\230\350\256\260\345\275\225/index.xml" index 8a4c999..904e92f 100644 --- "a/categories/csp\345\210\267\351\242\230\350\256\260\345\275\225/index.xml" +++ "b/categories/csp\345\210\267\351\242\230\350\256\260\345\275\225/index.xml" @@ -34,8 +34,12 @@ Tue, 14 Mar 2023 17:15:09 +0800 菜菜 https://imcaicai.github.io/csp202209/ - + 【CSP】202212题解 @@ -43,9 +47,9 @@ Tue, 14 Mar 2023 17:15:01 +0800 菜菜 https://imcaicai.github.io/csp202212/ - + diff --git "a/categories/labuladong\347\232\204\347\256\227\346\263\225\347\247\230\347\261\215/index.xml" "b/categories/labuladong\347\232\204\347\256\227\346\263\225\347\247\230\347\261\215/index.xml" index 533667c..f85a7c9 100644 --- "a/categories/labuladong\347\232\204\347\256\227\346\263\225\347\247\230\347\261\215/index.xml" +++ "b/categories/labuladong\347\232\204\347\256\227\346\263\225\347\247\230\347\261\215/index.xml" @@ -11,7 +11,6 @@ https://imcaicai.github.io/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/ diff --git a/csp202209/index.html b/csp202209/index.html index 4ed19c3..c3d730b 100644 --- a/csp202209/index.html +++ b/csp202209/index.html @@ -5,8 +5,12 @@ 【CSP】202209题解 - 菜菜的秘密花园 - + @@ -16,8 +20,12 @@ - + @@ -30,7 +38,7 @@ "mainEntityOfPage": { "@type": "WebPage", "@id": "https:\/\/imcaicai.github.io\/csp202209\/" - },"image": ["https:\/\/imcaicai.github.io\/img\/avatar.jpg"],"genre": "posts","keywords": "CSP, C\u002b\u002b","wordcount": 518 , + },"image": ["https:\/\/imcaicai.github.io\/img\/avatar.jpg"],"genre": "posts","keywords": "CSP, C\u002b\u002b","wordcount": 473 , "url": "https:\/\/imcaicai.github.io\/csp202209\/","datePublished": "2023-03-14T17:15:09+08:00","dateModified": "2023-03-14T17:15:09+08:00","license": "This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.","publisher": { "@type": "Organization", "name": "xxxx","logo": "https:\/\/imcaicai.github.io\/img\/avatar.jpg"},"author": { @@ -124,7 +132,7 @@

Contents

【CSP】202209题解

2

-

错误的:

-
- -
-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-30
-31
-32
-33
-34
-35
-36
-37
-38
-39
-40
-41
-
-
#include <iostream>
-#include <cmath>
-// #define _USE_MATH_DEFINES 
-
-using namespace std;
-int main()
-{
-
-	// 输入书本数量 n,包邮条件 x 
-	int n,x;
-	scanf("%d",&n);
-	scanf("%d",&x);
-	
-	// 输入书本价格,计算总和 sum 
-	int a[n];
-	int sum=0;
-	for(int i=0;i<n;i++){
-		scanf("%d",&a[i]);
-		sum+=a[i];
-	} 
-	
-	// 动态规划,找前 i 本书的价格和最大,不超过 sum-x 
-	int d[n][sum-x+1]={0};
-	for(int j=0;j<sum-x+1;j++){
-		d[0][j]=a[0]>j ? 0 : a[0];
-	}
-	
-	for(int i=1;i<n;i++){
-		for(int j=0;j<sum-x+1;j++){
-			if(d[i-1][j-a[i]]+a[i] <=j){
-				d[i][j]=max(d[i-1][j-a[i]]+a[i],d[i-1][j]);
-			}
-			else d[i][j]=d[i-1][j];
-			
-		}
-	}
-	
-
-	printf("\n%d",sum-d[n-1][sum-x]);
-	
-} 
-
-
-

正确的:

+

1 如此编码

+

🔗 题目:如此编码

+

本来看到题目很迷茫来着,想着第一题怎么就那么难,后来发现题目最后有提示,看完之后醍醐灌顶, 【取模】 确实是很妙的思路,可以积累下来。

+

/img/CSP/1.png

+

代码忘存了QAQ。

+

2 何以包邮

+

🔗 题目:何以包邮

+

🔴 【动态规划】sum 为所有参考书价格总和,题目可以理解为在 sum-x 价格内,最大化被删除的书价格总和,这样就可以把这个问题看作经典的 01背包问题 。(要学会转化💥💥💥)

 1
@@ -269,16 +202,11 @@ 

Contents

35 36 37 -38 -39 -40 -41
#include <iostream>
 #include <cmath>
-// #define _USE_MATH_DEFINES 
-
+
 using namespace std;
 int main()
 {
@@ -304,21 +232,31 @@ 

Contents

for(int i=1;i<n;i++){ for(int j=0;j<sum-x+1;j++){ - if(a[i] <=j){ - d[i][j]=max(d[i-1][j-a[i]]+a[i],d[i-1][j]); + if(a[i] <=j){ // 注意这个条件 + d[i][j]=max(d[i-1][j-a[i]]+a[i],d[i-1][j]); } else d[i][j]=d[i-1][j]; } } - - printf("%d",sum-d[n-1][sum-x]); - }
-

3

+

3 防疫大数据

+

🔗 题目:防疫大数据

+

🟠🟠🟠 这道题读题有点绕,最重要的是选择合适的数据结构,然后一步一步分析。重要结构如下:

+

✅ 首先考虑漫游数据 <d,u,r>,我们用结构 my 来存储,并构造一个 my 类型的容器存储风险用户: vector<my> u1 。这里之所以把 <d,u,r> 都存下来而不是只存 u,是为了方便后续判断用户某时某地的访问数据是否还有风险。

+

✅ 考虑风险地区。由于是否有风险是由日期、地区共同决定的, 我们使用 map<pair<int,int>,bool> mp 来表示某时某地是否有风险。 pair<int,int> 类型变量为键,存储地区、日期; bool 型变量为值,存储是否有风险。

+

✅ 由于最终结果要按用户编号从小到大输出,且同一用户只能输出一次,所以我们 选用有自动排序、自动去重功能的 set 容器

+

🟡🟡🟡 解题步骤:

+

✅ 清除 mp 中日期超过 7 天的记录,提高效率。( 注意删除时要用 it1,直接 erase(iter) 会报错 ,虽然我还不知道为什么QAQ)

+

✅ 添加风险地区: mp[{p,j}]=1

+

✅ 更新以前日期的漫游数据中的风险用户。因为日期更新了一天,所以先前存的风险用户可能已经不再有风险了,需要更新。注意题目要求是 【对所有的 D∈[j.d, i],地区 j.r 都在风险范围内】 ,中间有任意一天不属于风险用户也要丢掉, 所以不能只看 j.d 或 i 那天是否为风险用户 。(如果中间某天非风险,则说明原来那条漫游数据无风险,只是后面某天用户又去了同一个地点,那个地点刚好仍有风险。这里有点绕,很容易出错QAQ)

+

✅ 更新今天漫游数据中的风险用户。和上一步的方法类似。

+

✅ 输出答案。取 u1 中的 u 加入答案 ans 中输出。之所以不直接输出 u1 中的 u ,是因为题目要求最终结果由小到大排列且无重复用户。

+

🔵🔵🔵 在步骤 3 中,我们新开了一个 vector<my> u2 来存放符合要求的数据,并让 u1=u2,而不是在 u1 中直接删改,是因为可以避免频繁修改数组,提高效率

+

🟣🟣🟣 这种要求复杂、代码变量较多的题目,一定要自习检查变量是否用错,好几个 bug 都是因为容器索引的参数写错。 💢💢💢

 1
@@ -402,105 +340,87 @@ 

Contents

79 80 81 -82 -83 -84 -85 -86 -87 -88 -89 -90
#include <bits/stdc++.h>
-
-using namespace std;
+using namespace std;
 
-struct Node{			// 一条漫游数据 
-	int d;
-	int u;
-	int r;
-};
-
-int n;
-map<pair<int,int>,bool> mp;		// 键:地点+日期,值:是否是风险地区 
-vector<Node> user;				// 存放有风险的用户漫游数据 
+struct my{
+	int d;int u;int r;		// 一条漫游数据 
+};
+vector<my> u1;				// 记录当天有风险的用户的漫游数据,因为不能重复存储,所以用vector 
+map<pair<int,int>,bool> mp;	// 记录有风险的地区。键:地区、日期,值:是否是风险地 
 
 int main()
 {
-	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
-	cin >> n;					// 输入天数 n 
+	int n,r,m,p,di,ui,ri;
+	cin>>n;					// 输入天数 n
 	for(int i=0;i<n;i++){
-		int ri,mi;
-		cin >> ri >> mi;		// 输入每天的风险地数量、漫游数据量 
-
-		// 先clear
-		// map<pair<int,int>,bool> tmp;
+		cin>>r>>m;			// 输入风险地数量、漫游数据数量
+		
+		
+		// 1. 清除 mp 中日期超过 7 天的记录
 		for(map<pair<int,int>,bool>::iterator iter = mp.begin();iter!=mp.end();){
-			map<pair<int,int>,bool>::iterator it1 = iter;
-			iter++;
-			if(i - ((it1->first).second) >= 7)	mp.erase(it1);	// 每次新的一天,检查是否有 7 天外的风险地区数据,有就删掉						
-		}
-
-
-		// 插入合法的
-		for(int j=1;j<=ri;j++){						// 插入风险地区 
-			int pij;
-			cin >> pij;
-			for(int date=i;date<i+7;date++){	
-				mp[{pij,date}] = 1;					// 根据地区、时间标记风险地区 
-			}
+			// 注意这里要用 it1,直接 erase(iter) 会报错 
+			map<pair<int,int>,bool>::iterator it1 = iter;		
+			iter++;												
+			// 每次新的一天,检查是否有 7 天外的风险地区数据,有就删掉
+			if(i - ((it1->first).second) >= 7)	mp.erase(it1);							
 		}
-
-		// 筛选不合法的								// 以前的风险用户 
-		vector<Node> vc;
-		for(auto t:user){							// 遍历漫游数据 
-			bool f = 1;
-			int r = t.r;
-			if(i - t.d >= 7)	continue;			// 只看近 7 天的数据 
-
-			for(int date = t.d;date<=i;date++){
-				if(!mp.count({r,date})){			// 该地区和日期在风险范围内 
-					f = 0;
-					break;
+		
+		
+		// 2. 添加风险地区 
+		for(int j=0;j<r;j++){
+			cin>>p;
+			for(int j=i;j<i+7;j++)
+				mp[{p,j}]=1;			// 这里的参数原来写错了 
+		}
+		
+		
+		// 3. 更新以前日期的漫游数据中的风险用户
+		vector<my> u2;
+		for(auto j:u1){					// 这里原来写错了,是 u1 而不是 u2 
+			if(i-j.d >= 7)				// 已经是 7 天前的数据了 
+				continue;
+			
+			int flag=1;
+			for(int k=j.d;k<=i;k++){
+				if(!mp[{j.r, k}]){		// 不满足:对所有的 D∈[j.d, i],地区 j.r 都在风险范围内 
+					flag=0;break;
 				}
 			}
-			if(f)		vc.push_back(t);
+			
+			if(flag)					// 只插入从漫游时间到现在,都满足风险的数据 
+				u2.push_back(j); 
 		}
-		user.clear();
-		user = vc;									// 每一天都要更新风险用户列表 
-
-		// 必须是七天以内收到的漫游数据				// 今天的风险用户 
-		for(int j=1;j<=mi;j++){
-			int d,u,r;
-			cin >> d >> u >> r;
-
-			if(i - d >= 7)	continue;				// 7 天外的数据就丢掉 
-			// check 合法性
-			// 从 d 到 i 天地区r均为风险地区即可
-			bool f = 1;
-			for(int k=d;k<=i;k++){
-				if(!mp.count({r,k})){				// 在风险记录内,就加进来 
-					f = 0;
-					break;
+		u1.clear();						// 更新 u1
+		u1=u2;
+		
+		
+		// 4. 更新今天漫游数据中的风险用户 
+		for(int j=0;j<m;j++){
+			cin>>di>>ui>>ri;
+			if(i-di>=7)	continue;		// 7 天外的数据不用考虑
+			int flag=1;
+			for(int k=di;k<=i;k++){
+				if(!mp[{ri, k}]){		// 不满足:对所有的 D∈[j.d, i],地区 j.r 都在风险范围内 
+					flag=0;break;
 				}
 			}
-
-			if(f)	user.push_back({d,u,r});
-		}
-
-		set<int> ans;
-		for(auto x:user)	ans.insert(x.u);		// 只取用户 
-
-		cout << i << ' ';
-		for(auto x:ans){
-			cout << x << ' ';
+			
+			if(flag)
+				u1.push_back({di,ui,ri});
 		}
-		cout << '\n';
-	}
-
-
+		 
+		
+		// 5. 输出答案
+		set<int> ans;
+		for(auto j:u1)	ans.insert(j.u);	// 只取用户,同时满足用户按顺序输出 
+		cout<< i << ' ';
+		for(auto j:ans)	cout<< j <<' ';
+		cout<< '\n'; 
+		 
+	} 
 	return 0;
 }
 
diff --git a/csp202209/index.md b/csp202209/index.md index 8b2ef42..24aacf2 100644 --- a/csp202209/index.md +++ b/csp202209/index.md @@ -1,60 +1,25 @@ # 【CSP】202209题解 -## 2 +## 1 如此编码 -错误的: +🔗 **题目:[如此编码](http://118.190.20.162/view.page?gpid=T153)** -```c++ -#include -#include -// #define _USE_MATH_DEFINES +本来看到题目很迷茫来着,想着第一题怎么就那么难,后来发现题目最后有提示,看完之后醍醐灌顶, **【取模】** 确实是很妙的思路,可以积累下来。 -using namespace std; -int main() -{ +![img1](/img/CSP/1.png) - // 输入书本数量 n,包邮条件 x - int n,x; - scanf("%d",&n); - scanf("%d",&x); - - // 输入书本价格,计算总和 sum - int a[n]; - int sum=0; - for(int i=0;ij ? 0 : a[0]; - } - - for(int i=1;i #include -// #define _USE_MATH_DEFINES using namespace std; int main() @@ -81,111 +46,125 @@ int main() for(int i=1;i,我们用结构 my 来存储,并构造一个 my 类型的容器存储风险用户: `vector u1` 。这里之所以把 都存下来而不是只存 u,是为了方便后续判断用户某时某地的访问数据是否还有风险。 + +✅ 考虑风险地区。由于是否有风险是由日期、地区共同决定的, **我们使用 `map,bool> mp` 来表示某时某地是否有风险。** `pair` 类型变量为键,存储地区、日期; `bool` 型变量为值,存储是否有风险。 + +✅ 由于最终结果要按用户编号从小到大输出,且同一用户只能输出一次,所以我们 **选用有自动排序、自动去重功能的 set 容器** 。 + +🟡🟡🟡 解题步骤: + +✅ 清除 mp 中日期超过 7 天的记录,提高效率。( **注意删除时要用 it1,直接 erase(iter) 会报错** ,虽然我还不知道为什么QAQ) + +✅ 添加风险地区: `mp[{p,j}]=1` + +✅ 更新以前日期的漫游数据中的风险用户。因为日期更新了一天,所以先前存的风险用户可能已经不再有风险了,需要更新。注意题目要求是 **【对所有的 D∈[j.d, i],地区 j.r 都在风险范围内】** ,中间有任意一天不属于风险用户也要丢掉, **所以不能只看 j.d 或 i 那天是否为风险用户** 。(如果中间某天非风险,则说明原来那条漫游数据无风险,只是后面某天用户又去了同一个地点,那个地点刚好仍有风险。这里有点绕,很容易出错QAQ) + +✅ 更新今天漫游数据中的风险用户。和上一步的方法类似。 + +✅ 输出答案。取 u1 中的 u 加入答案 ans 中输出。之所以不直接输出 u1 中的 u ,是因为题目要求最终结果由小到大排列且无重复用户。 + +🔵🔵🔵 在步骤 3 中,我们新开了一个 `vector u2` 来存放符合要求的数据,并让 u1=u2,而不是在 u1 中直接删改,是因为可以避免频繁修改数组,提高效率 + +🟣🟣🟣 这种要求复杂、代码变量较多的题目,一定要自习检查变量是否用错,好几个 bug 都是因为容器索引的参数写错。 💢💢💢 ```c++ #include - using namespace std; -struct Node{ // 一条漫游数据 - int d; - int u; - int r; +struct my{ + int d;int u;int r; // 一条漫游数据 }; - -int n; -map,bool> mp; // 键:地点+日期,值:是否是风险地区 -vector user; // 存放有风险的用户漫游数据 +vector u1; // 记录当天有风险的用户的漫游数据,因为不能重复存储,所以用vector +map,bool> mp; // 记录有风险的地区。键:地区、日期,值:是否是风险地 int main() { - ios::sync_with_stdio(false);cin.tie(0);cout.tie(0); - cin >> n; // 输入天数 n + int n,r,m,p,di,ui,ri; + cin>>n; // 输入天数 n for(int i=0;i> ri >> mi; // 输入每天的风险地数量、漫游数据量 - - // 先clear - // map,bool> tmp; + cin>>r>>m; // 输入风险地数量、漫游数据数量 + + + // 1. 清除 mp 中日期超过 7 天的记录 for(map,bool>::iterator iter = mp.begin();iter!=mp.end();){ - map,bool>::iterator it1 = iter; - iter++; - if(i - ((it1->first).second) >= 7) mp.erase(it1); // 每次新的一天,检查是否有 7 天外的风险地区数据,有就删掉 + // 注意这里要用 it1,直接 erase(iter) 会报错 + map,bool>::iterator it1 = iter; + iter++; + // 每次新的一天,检查是否有 7 天外的风险地区数据,有就删掉 + if(i - ((it1->first).second) >= 7) mp.erase(it1); } - - - // 插入合法的 - for(int j=1;j<=ri;j++){ // 插入风险地区 - int pij; - cin >> pij; - for(int date=i;date>p; + for(int j=i;j vc; - for(auto t:user){ // 遍历漫游数据 - bool f = 1; - int r = t.r; - if(i - t.d >= 7) continue; // 只看近 7 天的数据 - - for(int date = t.d;date<=i;date++){ - if(!mp.count({r,date})){ // 该地区和日期在风险范围内 - f = 0; - break; + + + // 3. 更新以前日期的漫游数据中的风险用户 + vector u2; + for(auto j:u1){ // 这里原来写错了,是 u1 而不是 u2 + if(i-j.d >= 7) // 已经是 7 天前的数据了 + continue; + + int flag=1; + for(int k=j.d;k<=i;k++){ + if(!mp[{j.r, k}]){ // 不满足:对所有的 D∈[j.d, i],地区 j.r 都在风险范围内 + flag=0;break; } } - if(f) vc.push_back(t); + + if(flag) // 只插入从漫游时间到现在,都满足风险的数据 + u2.push_back(j); } - user.clear(); - user = vc; // 每一天都要更新风险用户列表 - - // 必须是七天以内收到的漫游数据 // 今天的风险用户 - for(int j=1;j<=mi;j++){ - int d,u,r; - cin >> d >> u >> r; - - if(i - d >= 7) continue; // 7 天外的数据就丢掉 - // check 合法性 - // 从 d 到 i 天地区r均为风险地区即可 - bool f = 1; - for(int k=d;k<=i;k++){ - if(!mp.count({r,k})){ // 在风险记录内,就加进来 - f = 0; - break; + u1.clear(); // 更新 u1 + u1=u2; + + + // 4. 更新今天漫游数据中的风险用户 + for(int j=0;j>di>>ui>>ri; + if(i-di>=7) continue; // 7 天外的数据不用考虑 + int flag=1; + for(int k=di;k<=i;k++){ + if(!mp[{ri, k}]){ // 不满足:对所有的 D∈[j.d, i],地区 j.r 都在风险范围内 + flag=0;break; } } - - if(f) user.push_back({d,u,r}); + + if(flag) + u1.push_back({di,ui,ri}); } - + + + // 5. 输出答案 set ans; - for(auto x:user) ans.insert(x.u); // 只取用户 - - cout << i << ' '; - for(auto x:ans){ - cout << x << ' '; - } - cout << '\n'; - } - - + for(auto j:u1) ans.insert(j.u); // 只取用户,同时满足用户按顺序输出 + cout<< i << ' '; + for(auto j:ans) cout<< j <<' '; + cout<< '\n'; + + } return 0; } ``` diff --git a/csp202212/index.html b/csp202212/index.html index b676f39..384d99c 100644 --- a/csp202212/index.html +++ b/csp202212/index.html @@ -5,9 +5,9 @@ 【CSP】202212题解 - 菜菜的秘密花园 - + @@ -17,9 +17,9 @@ - + @@ -32,7 +32,7 @@ "mainEntityOfPage": { "@type": "WebPage", "@id": "https:\/\/imcaicai.github.io\/csp202212\/" - },"image": ["https:\/\/imcaicai.github.io\/img\/avatar.jpg"],"genre": "posts","keywords": "CSP, C\u002b\u002b","wordcount": 921 , + },"image": ["https:\/\/imcaicai.github.io\/img\/avatar.jpg"],"genre": "posts","keywords": "CSP, C\u002b\u002b","wordcount": 587 , "url": "https:\/\/imcaicai.github.io\/csp202212\/","datePublished": "2023-03-14T17:15:01+08:00","dateModified": "2023-03-14T17:15:01+08:00","license": "This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.","publisher": { "@type": "Organization", "name": "xxxx","logo": "https:\/\/imcaicai.github.io\/img\/avatar.jpg"},"author": { @@ -126,8 +126,8 @@

Contents

【CSP】202212题解

1

-

错误的

-
- -
-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-
-
#include <iostream>
-using namespace std;
-int main()
-{
-	// 输入年数n,年利率i 
-	int n;
-	double i;
-	scanf("%d %lf", &n,&i);
-	
-	double s=0;	// 保存最后结果 
-	int a;	// 每年收益 
-	for(int j=0;j<n+1;j++){
-		scanf("%d", &a);
-		s=s/(1+i) + a;
-	}
-	
-	printf("%f", s);
-	
-	
-} 
-
-
-

正确的

+

1 现值计算

+

🔗 题目:现值计算

+

🔴 注意 s=s/(1+i) + a[j] 是从后往前更新的,一开始写成从 a[0] 到 a[n] 出了 bug。

 1
@@ -215,8 +169,6 @@ 

Contents

20 21 22 -23 -24
#include <iostream>
@@ -240,13 +192,11 @@ 

Contents

} printf("%f", s); - - }
-

2

-

正确代码

+

2 训练计划

+

🔗 题目:训练计划

 1
@@ -313,8 +263,6 @@ 

Contents

62 63 64 -65 -66
#include <iostream>
@@ -380,248 +328,13 @@ 

Contents

for(int i=1;i<=m;i++) printf("%d ",last[i]); } - - -} -
-
-

3

-

错误代码(仍然不知道哪里错了

-
- -
-
  1
-  2
-  3
-  4
-  5
-  6
-  7
-  8
-  9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
-100
-101
-102
-103
-104
-105
-106
-107
-108
-109
-110
-111
-112
-113
-114
-
-
#include <iostream>
-#include <cmath>
-// #define _USE_MATH_DEFINES 
-
-using namespace std;
-int main()
-{
-
-	// 输入量化矩阵 Q
-	int Q[8][8];
-	for(int i=0;i<8;i++){
-		for(int j=0;j<8;j++){
-			scanf("%d",&Q[i][j]);
-		}
-	} 
-	
-	// 输入扫描个数 n,任务 T
-	int n,T;
-	scanf("%d",&n);
-	scanf("%d",&T);
-	
-	// 输入一组扫描数据
-	int d[n];
-	for(int i=0;i<n;i++){
-		scanf("%d",&d[i]);
-	} 
-	
-	// 将扫描数据放入填充矩阵 M 中 
-	int M[8][8]={0};
-	for(int i=1,con=0;con<n;i++){
-		if(i%2==1){
-			for(int j=i-1;j>=0 && con<n;j--){
-				M[j][i-1-j]=d[con];
-				con++;
-			}
-		}
-		else{
-			for(int j=0;j<=i-1 && con<n;j++){
-				M[j][i-1-j]=d[con];
-				con++;
-			}
-			
-		}
-	}
-	
-	// 输出 M 矩阵
-	if(T==0){
-		for(int i=0;i<8;i++){
-			for(int j=0;j<8;j++){
-				printf("%d ", M[i][j]);
-			}
-		
-			printf("\n");
-		} 
-		
-		return 0;
-	}
-	
-	// 计算与量化矩阵 Q 相乘后的矩阵 M
-	for(int i=0;i<8;i++){
-		for(int j=0;j<8;j++){
-			M[i][j]=M[i][j]*Q[i][j];
-		}
-	} 
-	
-	// 输出与量化矩阵 Q 相乘后的矩阵 M
-	if(T==1){
-		for(int i=0;i<8;i++){
-			for(int j=0;j<8;j++){
-				printf("%d ", M[i][j]);
-			}
-		
-			printf("\n");
-		} 
-		
-		return 0;
-	}
-	
-	// 对 M 进行离散余弦逆变换 
-	int MM[8][8];
-	for(int i=0;i<8;i++){
-		for(int j=0;j<8;j++){
-			double s=0;
-			for(int u=0;u<8;u++){
-				for(int v=0;v<8;v++){
-					double temp=cos(acos(-1)*u*(i+0.5)/8)*cos(acos(-1)*v*(j+0.5)/8)*M[u][v];
-					if(u==0)
-						temp *= pow(0.5,0.5);
-					if(v==0)
-						temp *= pow(0.5,0.5);
-					s+=temp;
-				}
-			} 
-			s=s/4;
-			MM[i][j]=(int)(s+128.5);
-			MM[i][j] = MM[i][j]>255 ? 255 : MM[i][j];
-			MM[i][j] = MM[i][j]<0 ? 0 : MM[i][j];
-		}
-	} 
-	
-	// 输出与量化矩阵 Q 相乘后的矩阵 M
-	if(T==2){
-		for(int i=0;i<8;i++){
-			for(int j=0;j<8;j++){
-				printf("%d ", MM[i][j]);
-			}
-		
-			printf("\n");
-		} 
-		
-		return 0;
-	}
-	
 } 
 
-

正确代码:

+

3 JPEG解码

+

🔗 题目:JPEG解码

+

🟠 这种题目难度不是很大,但题目很长,需要把要求一步步分解,耐心计算即可。

+

🟡 蛇形矩阵填充,由于本题矩阵大小固定,可以 用矩阵 idx[8][8] 来存储对应位置填充的元素下标 ,就可以很方便的完成存储。

  1
@@ -735,8 +448,6 @@ 

Contents

109 110 111 -112 -113
#include <iostream>
@@ -789,7 +500,6 @@ 

Contents

for(int j=0;j<8;j++){ printf("%d ", M[i][j]); } - printf("\n"); } @@ -844,7 +554,6 @@

Contents

for(int j=0;j<8;j++){ printf("%d ", MM[i][j]); } - printf("\n"); } diff --git a/csp202212/index.md b/csp202212/index.md index 3d7a4d4..c7b7d2c 100644 --- a/csp202212/index.md +++ b/csp202212/index.md @@ -1,34 +1,11 @@ # 【CSP】202212题解 -## 1 +## 1 现值计算 -错误的 +🔗 **题目:[现值计算](http://118.190.20.162/view.page?gpid=T160)** -```c++ -#include -using namespace std; -int main() -{ - // 输入年数n,年利率i - int n; - double i; - scanf("%d %lf", &n,&i); - - double s=0; // 保存最后结果 - int a; // 每年收益 - for(int j=0;j @@ -52,14 +29,12 @@ int main() } printf("%f", s); - - } ``` -## 2 +## 2 训练计划 -正确代码 +🔗 **题目:[训练计划](http://118.190.20.162/view.page?gpid=T159)** ```c++ #include @@ -125,133 +100,16 @@ int main() for(int i=1;i<=m;i++) printf("%d ",last[i]); } - - } ``` -## 3 - -错误代码(仍然不知道哪里错了 - -```c++ -#include -#include -// #define _USE_MATH_DEFINES +## 3 JPEG解码 -using namespace std; -int main() -{ +🔗 **题目:[JPEG解码](http://118.190.20.162/view.page?gpid=T158)** - // 输入量化矩阵 Q - int Q[8][8]; - for(int i=0;i<8;i++){ - for(int j=0;j<8;j++){ - scanf("%d",&Q[i][j]); - } - } - - // 输入扫描个数 n,任务 T - int n,T; - scanf("%d",&n); - scanf("%d",&T); - - // 输入一组扫描数据 - int d[n]; - for(int i=0;i=0 && con255 ? 255 : MM[i][j]; - MM[i][j] = MM[i][j]<0 ? 0 : MM[i][j]; - } - } - - // 输出与量化矩阵 Q 相乘后的矩阵 M - if(T==2){ - for(int i=0;i<8;i++){ - for(int j=0;j<8;j++){ - printf("%d ", MM[i][j]); - } - - printf("\n"); - } - - return 0; - } - -} -``` +🟠 这种题目难度不是很大,但题目很长,需要把要求一步步分解,耐心计算即可。 -正确代码: +🟡 蛇形矩阵填充,由于本题矩阵大小固定,可以 **用矩阵 idx\[8][8] 来存储对应位置填充的元素下标** ,就可以很方便的完成存储。 ```c++ #include @@ -304,7 +162,6 @@ int main() for(int j=0;j<8;j++){ printf("%d ", M[i][j]); } - printf("\n"); } @@ -359,7 +216,6 @@ int main() for(int j=0;j<8;j++){ printf("%d ", MM[i][j]); } - printf("\n"); } diff --git a/img/CSP/1.png b/img/CSP/1.png new file mode 100644 index 0000000..1785e15 Binary files /dev/null and b/img/CSP/1.png differ diff --git a/index.html b/index.html index 0831127..1f5d13c 100644 --- a/index.html +++ b/index.html @@ -132,7 +132,6 @@ 【动态规划】动态规划核心框架
🔴 【动态规划三要素】重叠子问题、最优子结构、状态转移方程 🟢 【思维框架】明确 base case → 明确「状态」→ 明确「选择」 → 定义 dp 数组/函数的含义。 -🔵 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # ⾃顶向下递归的动态规划 def dp(状态1, 状态2, ...): for 选择 in 所有可能的选择: # 此时的状态已经因为做了选择⽽改变 result = 求最值(result, dp(状态1, 状态2, ...)) return result # ⾃底向上迭代的动态规划 # 初始化 base case dp[0][0][...] = base case # 进⾏状态转移 for 状态1 in 状态1的所有取值: for 状态2 in 状态2的所有取值: for .
@@ -213,15 +212,19 @@  CSPC++

【CSP】202209题解 -

2 错误的: -1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include <iostream>#include <cmath>// #define _USE_MATH_DEFINES using namespace std; int main() { // 输入书本数量 n,包邮条件 x int n,x; scanf("%d",&n); scanf("%d",&x); // 输入书本价格,计算总和 sum int a[n]; int sum=0; for(int i=0;i<n;i++){ scanf("%d",&a[i]); sum+=a[i]; } // 动态规划,找前 i 本书的价格和最大,不超过 sum-x int d[n][sum-x+1]={0}; for(int j=0;j<sum-x+1;j++){ d[0][j]=a[0]>j ?

【CSP】202212题解 -

1 错误的 -1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream>using namespace std; int main() { // 输入年数n,年利率i int n; double i; scanf("%d %lf", &n,&i); double s=0; // 保存最后结果 int a; // 每年收益 for(int j=0;j<n+1;j++){ scanf("%d", &a); s=s/(1+i) + a; } printf("%f", s); } 正确的 -1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream>using namespace std; int main() { // 输入年数n,年利率i int n; double i; scanf("%d", &n); scanf("%lf",&i); double s=0; // 保存最后结果 int a[n+1]; // 每年收益 for(int j=0;j<n+1;j++){ scanf("%d", &a[j]); } for(int j=n;j>=0;j--){ s=s/(1+i) + a[j]; } printf("%f", s); } 2 正确代码
  • diff --git a/index.json b/index.json index 73c1b66..75b87d1 100644 --- a/index.json +++ b/index.json @@ -1 +1 @@ -[{"categories":["C++"],"content":"1 简介 🟠 pair 容器将 2 个普通元素 first 和 second(C++ 基本数据类型、结构体、类自定的类型等)创建成一个新元素\u003cfirst, second\u003e。 🔵 使用需加上头文件:#include \u003cutility\u003e ","date":"2023-03-18","objectID":"/stlpair%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:1:0","tags":["C++","STL","pair"],"title":"【STL】pair容器用法","uri":"/stlpair%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"2 创建map容器 1️⃣ 调用 pair 容器类的默认构造函数。 pair \u003cstring, int\u003e pair1; 2️⃣ 在创建 pair 容器的同时,进行初始化。 pair \u003cstring, int\u003e pair2(\"语文\",90); 3️⃣ 利用先前已创建好的 pair 容器和拷贝构造函数,再创建一个新的 pair 容器。 pair \u003cstring, int\u003e pair3(pair2); ","date":"2023-03-18","objectID":"/stlpair%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:2:0","tags":["C++","STL","pair"],"title":"【STL】pair容器用法","uri":"/stlpair%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"3 常见用法 3.1 手动为 pair 对象赋值 pair1.first = \"数学\"; pair1.second = \"100\"; 3.2 比较2个pair对象 先比较 pair.first 元素的大小,如果相等则继续比较 pair.second 元素的大小。对于进行比较的 2 个 pair 对象,其对应的键和值的类型比较相同 3.3 swap() 函数 能够互换 2 个 pair 对象的键值对,其操作成功的前提是这 2 个 pair 对象的键和值的类型要相同。 ","date":"2023-03-18","objectID":"/stlpair%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:3:0","tags":["C++","STL","pair"],"title":"【STL】pair容器用法","uri":"/stlpair%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"🔴 【动态规划三要素】重叠子问题、最优子结构、状态转移方程 🟢 【思维框架】明确 base case → 明确「状态」→ 明确「选择」 → 定义 dp 数组/函数的含义。 🔵 # ⾃顶向下递归的动态规划 def dp(状态1, 状态2, ...): for 选择 in 所有可能的选择: # 此时的状态已经因为做了选择⽽改变 result = 求最值(result, dp(状态1, 状态2, ...)) return result # ⾃底向上迭代的动态规划 # 初始化 base case dp[0][0][...] = base case # 进⾏状态转移 for 状态1 in 状态1的所有取值: for 状态2 in 状态2的所有取值: for ... dp[状态1][状态2][...] = 求最值(选择1,选择2...) ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:0:0","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"1 【重叠子问题】 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:0","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"题目——斐波那契数 力扣 509. 斐波那契数 斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。 给定 n ,请计算 F(n) 。 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:1","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"1.1 暴力递归 int fib(int N) { if (N == 1 || N == 2) return 1; return fib(N - 1) + fib(N - 2); } 子问题个数: O(2^n)。解决⼀个子问题的时间:只有 f(n - 1) + f(n - 2) ⼀个加法操作, 时间为 O(1)。因此这个算法的时间复杂度为二者相乘,即 O(2^n)。 这种方法存在大量重复计算,即重叠子问题,我们需要想办法解决这个问题。 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:2","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"1.2 带备忘录的递归解法(自顶向下) int fib(int N) { // 备忘录全初始化为 0 int[] memo = new int[N + 1]; // 进⾏带备忘录的递归 return helper(memo, N); } int helper(int[] memo, int n) { // base case if (n == 0 || n == 1) return n; // 已经计算过,不⽤再计算了 if (memo[n] != 0) return memo[n]; memo[n] = helper(memo, n - 1) + helper(memo, n - 2); return memo[n]; } 在这种算法中,由于不存在冗余计算,子问题个数为 O(n),解决一个子问题的时间仍为 O(1)。 所以算法的时间复杂度是 O(n),和暴力算法相比提升很多。 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:3","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"1.3 dp 数组的迭代/递推解法(自低向上) int fib(int N) { if (N == 0) return 0; int[] dp = new int[N + 1]; // base case dp[0] = 0; dp[1] = 1; // 状态转移 for (int i = 2; i \u003c= N; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[N]; } 根据斐波那契数列的状态转移方程( f(n) = f(n-1) + f(n-2) ),当前状态只和之前的两个状态有关,其实并不需要 用数组来存储所有状态,只要存储当前状态的前两个就行。 所以,可以进一步优化,把空间复杂度降为 O(1): int fib(int n) { if (n == 0 || n == 1) { // base case return n; } // 分别代表 dp[i - 1] 和 dp[i - 2] int dp_i_1 = 1, dp_i_2 = 0; for (int i = 2; i \u003c= n; i++) { // dp[i] = dp[i - 1] + dp[i - 2]; int dp_i = dp_i_1 + dp_i_2; // 滚动更新 dp_i_2 = dp_i_1; dp_i_1 = dp_i; } return dp_i_1; } ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:4","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"2 【转移方程】 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:5","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"题目——零钱兑换 力扣 322. 零钱兑换 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。 你可以认为每种硬币的数量是无限的。 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:6","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"2.1 暴力递归 🔴 确定 base case :目标金额 amount 为 0 时算法返回 0,因为不需要任何硬币就已经凑出目标金额了。 🟡 确定「状态」 ,也就是子问题中会变化的变量。在本题中是目标金额 amount。 🟢 确定「选择」 ,也就是导致「状态」产生变化的行为。在本题中为所有硬币的面值。 🔵 明确 dp 函数/数组的定义 。即凑出目标金额所需的最少硬币数量。 int coinChange(int[] coins, int amount) { // 题⽬要求的最终结果是 dp(amount) return dp(coins, amount) } // 定义:要凑出⾦额 n,⾄少要 dp(coins, n) 个硬币 int dp(int[] coins, int amount) { // base case if (amount == 0) return 0; if (amount \u003c 0) return -1; int res = Integer.MAX_VALUE; for (int coin : coins) { // 计算⼦问题的结果 int subProblem = dp(coins, amount - coin); // ⼦问题⽆解则跳过 if (subProblem == -1) continue; // 在⼦问题中选择最优解,然后加⼀ res = Math.min(res, subProblem + 1); } return res == Integer.MAX_VALUE ? -1 : res; } 假设目标金额为 n,给定的硬币个数为 k,那么子问题个数在 k^n 这个数量级。而解决每个子问题的复杂度为 O(k),相乘得到总时间复杂度为 O(k^n)。 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:7","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"2.2 带备忘录的递归 int[] memo; int coinChange(int[] coins, int amount) { memo = new int[amount + 1]; // 备忘录初始化为⼀个不会被取到的特殊值,代表还未被计算 Arrays.fill(memo, -666); return dp(coins, amount); } int dp(int[] coins, int amount) { if (amount == 0) return 0; if (amount \u003c 0) return -1; // 查备忘录,防⽌重复计算 if (memo[amount] != -666) return memo[amount]; int res = Integer.MAX_VALUE; for (int coin : coins) { // 计算⼦问题的结果 int subProblem = dp(coins, amount - coin); // ⼦问题⽆解则跳过 if (subProblem == -1) continue; // 在⼦问题中选择最优解,然后加⼀ res = Math.min(res, subProblem + 1); } // 把计算结果存⼊备忘录 memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res; return memo[amount]; } 子问题数目为 O(n)。处理⼀个子问题的时间不变,仍是 O(k),总的时间复杂度是 O(kn)。 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:8","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"2.3 dp 数组的迭代解法 dp 数组的定义:当目标金额为 i 时,至少需要 dp[i] 枚硬币凑出。 int coinChange(int[] coins, int amount) { int[] dp = new int[amount + 1]; // 数组⼤⼩为 amount + 1,初始值也为 amount + 1 Arrays.fill(dp, amount + 1); // base case dp[0] = 0; // 外层 for 循环在遍历所有状态的所有取值 for (int i = 0; i \u003c dp.length; i++) { // 内层 for 循环在求所有选择的最⼩值 for (int coin : coins) { // ⼦问题⽆解,跳过 if (i - coin \u003c 0) { continue; } dp[i] = Math.min(dp[i], 1 + dp[i - coin]); } } return (dp[amount] == amount + 1) ? -1 : dp[amount]; } ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:9","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["蓝桥杯刷题记录"],"content":"6 跑步锻炼 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:1:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 小蓝每天都锻炼身体。 正常情况下,小蓝每天跑 1 千米。如果某天是周一或者月初(1 日),为了激励自己,小蓝要跑 2 千米。如果同时是周一或月初,小蓝也是跑 2 千米。 小蓝跑步已经坚持了很长时间,从 2000 年 1 月 1 日周六(含)到 2020 年 10 月 1 日周四(含)。请问这段时间小蓝总共跑步多少千米? ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:1:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:30min 🎯难度:💡 大小月记不清。大月:1、3、5、7、8、10、12 闰年的判断方法:year%4==0 \u0026\u0026 year%100!=0) || year%400==0 注意闰年的2月是29天,不是28天!(因为这个调了好久 bug 💥💢) ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:1:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 #include \u003ciostream\u003eusing namespace std; // 判断是否是闰年,是则返回 true,否则返回 false bool isRun(int year){ if((year%4==0 \u0026\u0026 year%100!=0) || year%400==0) return true; return false; } int main(){ // 每个月对应的天数 int mon1[13]={0,31,29,31,30,31,30,31,31,30,31,30,31}; // 闰年 int mon2[13]={0,31,28,31,30,31,30,31,31,30,31,30,31}; // 非闰年 int cnt=6; // 用于判断是否是周一 int sum=0; // 计算总数 for(int y=2000;y\u003c=2020;y++){ // 遍历每一年 for(int m=1;m\u003c=12;m++){ // 遍历每一月 if(isRun(y)){ // 闰年 for(int d=1;d\u003c=mon1[m];d++){ if(d==1 || cnt%7==1) // 如果是月初或者周一要额外加 1 sum++; sum++; cnt=(cnt+1)%7; // 更新 cnt 的值 // 遍历到最后一天 2020.10.01 结束 if(y==2020 \u0026\u0026 m==10 \u0026\u0026 d==1){ printf(\"%d\\n\",sum); return 0; } } } else{ // 不是闰年 for(int d=1;d\u003c=mon2[m];d++){ if(d==1 || cnt%7==1) // 如果是月初或者周一要额外加 1 sum++; sum++; cnt=(cnt+1)%7; // 更新 cnt 的值 } } } } return 0; } ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:1:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"7 蛇形填数 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:2:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 如下图所示,小明用从 1 开始的正整数“蛇形”填充无限大的矩阵。 1 2 6 7 15 ... 3 5 8 14 ... 4 9 13 ... 10 12 ... 11 ... ... 容易看出矩阵第二行第二列中的数是 5。请你计算矩阵中第 20 行第 20 列的数是多少? ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:2:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:15min 🎯难度:💡 小学找规律题,可手算🎉🎉 要看清题目所给的下标是从0开始还是从1开始!(因为题中的矩阵行列从0开始算,找了好久bug 💢💥 注意题目要求。比如这道题只求(20, 20)的数,就只用找坐标为(a, a)的元素的规律,而不用把所有的都找出 找规律要容易出错,要多算几个验证 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:2:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 🟠 把所有坐标的元素表达式都找出来 这种做法比较普适,缺点是可能会浪费宝贵的答题时间,并且规律较多的情况会把自己绕晕,很容易疏忽一两个点导致答案不对。😭😤 我们先把每次填充的同一条斜线上的元素看成一组,很容易发现一些规律: 这条斜线上有 n 个元素,对每个元素 (i, j) 都有:i + j = n + 1(i,j 都是从1开始的❗❗❗) 当 n 为奇数时,从下往上填充,也就是 j 从 1 开始,比如:(3, 1),(2, 2),(1, 3)。偶数则相反。 元素值 = 它是第几个被填充的 那么,要求位置为 (20, 20) 的元素值,我们只需要知道他是第几个被填充的。 首先 20+20=40,说明 (20, 20) 所在的斜线上有39个元素,每个元素的坐标和都为40。又由于 39 是个奇数,所以这条斜线填充的顺序为:(39,1),(38,2),(37,3)…(20,20)…(1,39)。 (20,20) 是这条斜线上的第 20 个元素。而在这条斜线被开始填充前,已经填充的元素数为:1+2+3+4+5...+38 = 741 。最终可以得到 (20,20) 是第几个被填充的: 741+20=261 。 🟡 只考虑坐标为 (a, a) 的元素 先自己手动补充后面的元素,坐标形式为 (a, a) 的元素值依次为:1, 5, 13, 25, 41…… 仔细分析可以发现:1=4×0+1;5=4×1+1;13=4×3+1;25=4×6+1;41=4×10+1。除开表达式的相同点,接下来只用找 1, 3, 6, 10 的规律:1=1;3=1+2;6=1+2+3;10=1+2+3+4。由此我们很容易得到坐标为 (a,a) 的元素值表达式:4×(1+2+3+...a-1)+1 。 带入 a=20 得到最终答案:261。 ⚠ 此处要注意的是,一定要多写几个检验❗❗❗只看 1, 5, 13, 25 找规律就很容易出错❗❗❗ ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:2:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"8 既约分数 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:3:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 如果一个分数的分子和分母的最大公约数是 11,这个分数称为既约分数。 例如 3/4, 1/8, 7/1,都是既约分数。 请问,有多少个既约分数,分子和分母都是 11 到 2020 之间的整数(包括 11 和 2020)? ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:3:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:5min 🎯难度:💡 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:3:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 填空题,没什么好说的,直接暴力解决啦~ #include \u003ciostream\u003eusing namespace std; int main(){ int sum=0; for(int i=1;i\u003c=2020;i++){ // 分子 for(int j=1;j\u003c=2020;j++){ // 分母 int flag=1; for(int k=2;k\u003c=2020;k++){ if(i%k==0 \u0026\u0026 j%k==0){ flag=0;break; } } sum+=flag; } } cout\u003c\u003csum; return 0; } ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:3:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"9 数字三角形 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:4:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 上图给出了一个数字三角形。从三角形的顶部到底部有很多条不同的路径。对于每条路径,把路径上面的数加起来可以得到一个和,你的任务就是找到最大的和。 路径上的每一步只能从一个数走到下一层和它最近的左边的那个数或者右 边的那个数。此外,向左下走的次数与向右下走的次数相差不能超过 1。 输入描述 输入的第一行包含一个整数 N (1≤ N ≤100),表示三角形的行数。 下面的 N 行给出数字三角形。数字三角形上的数都是 0 至 100 之间的整数。 输出描述 输出一个整数,表示答案。 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:4:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:没写出来 🎯难度:💡💡 【求矩阵路径最值问题】动态规划、开一个数组记录到每个位置的最大/小值 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:4:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 这道题想了很久毫无头绪,是10道题里唯一不会写的,看了评论区一个大佬的题解后醍醐灌顶。核心思想: 🔴 用一个数组 s[i][j] 记录从 a[1][1]到 a[i][j] 的路径上的数字和最大值。关注点在结果位置,是 【上一跳从哪来】 而不是 【下一跳往哪走】 。 🔵 遍历三角形数组,用动态规划更新矩阵 s。base case为: s[1][1]=a[1][1] 。状态转移方程为 s[i][j]=a[i][j]+max(s[i-1][j-1],s[i-1][j]) 。( s[i][j] 处的最大值要么是从 [i-1][j-1] 过来的,要么是从 [i-1][j] 过来的) 🟢 由于要满足向左、向右的步数差不超过1,最终结果只能是最中间的。考虑n的奇偶性,n为奇数时,输出矩阵s最后一行最中间的,即 s[n][n/2+1] ;n为偶数时,矩阵s最后一行最中间有2个元素,输出值最大的那个,即 max(s[n][n/2],s[n][n/2+1]) 。 ⚠ 这个思路真的强推,在很多动态规划的题目中都能应用上,例如今天刚好做的:931. 下降路径最小和 #include \u003ciostream\u003eusing namespace std; int main() { // 读取数据 // s[i][j]:从 a[1][1] 遍历到 a[i][j] 的最大和 int a[105][105]={0},s[105][105]={0},n; scanf(\"%d\", \u0026n); for(int i=1;i\u003c=n;i++){ for(int j=1;j\u003c=i;j++){ scanf(\"%d\",\u0026a[i][j]); } } // 计算最大和 s[1][1]=a[1][1]; for(int i=2;i\u003c=n;i++){ for(int j=1;j\u003c=i;j++){ s[i][j]=a[i][j]+max(s[i-1][j-1],s[i-1][j]); } } // n 是偶数且要满足向左、向右的步数差不超过 1,只能是中间两个 // n 是奇数且要满足向左、向右的步数差不超过 1,只能是最中间的那个 if(n%2==0) cout\u003c\u003cmax(s[n][n/2],s[n][n/2+1]); // 注意 s 是二维矩阵,不能写错成一维的 else cout\u003c\u003cs[n][n/2+1]; return 0; } ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:4:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"10 回文日期 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:5:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 2020 年春节期间,有一个特殊的日期引起了大家的注意:2020 年 2 月 2 日。因为如果将这个日期按 “yyyymmdd” 的格式写成一个 8 位数是 20200202,恰好是一个回文数。我们称这样的日期是回文日期。 给定一个 8 位数的日期,请你计算该日期之后下一个回文日期和下一个 ABABBABA 型的回文日期各是哪一天。 输入描述 输入包含一个八位整数 N,表示日期。 对于所有评测用例,10000101≤ N ≤89991231,保证 N 是一个合法日期的 8 位数表示。 输出描述 输出两行,每行 1 个八位数。第一行表示下一个回文日期,第二行表示下一个 ABABBABA 型的回文日期。 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:5:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"答题情况 ⏰解题耗时:60min 🎯难度:💡 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:5:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 虽然官方给这道题贴的标签是 中等 ,但感觉难度一般,暴力穷举就好。不过确实很容易出错QAQ。 🔴 计算第1个回文日期时,即 ABCDDCBA 型,只用根据 ABCD 来枚举,需要确保两点:1️⃣ ABCDDCBA 的值需大于输入的 起始日期date 2️⃣ ABCDDCBA 需要是一个合法日期。注意 D 的值只能是0或1,可以减少枚举量。 🔵 计算第2个回文日期时,即 ABABBABA 型,只用根据 AB 来枚举,条件和上面类似。不过由于B只能取0或1,又只有输出第一个符合的回文日期,A的值要么是a,要么是a+1。(a是输入日期 date 的最高位) 🟢 判断是否是合法日期,只需把日期拆成年、月、日,看月、日的值是否合法,注意需要看年份是否是闰年。 #include \u003ciostream\u003eusing namespace std; // 用于判断输入的值是否是一个合格日期 bool isDate(int y,int m,int d){ int mon1[13]={0,31,29,31,30,31,30,31,31,30,31,30,31}; // 闰年 int mon2[13]={0,31,28,31,30,31,30,31,31,30,31,30,31}; // 非闰年 if((y%4==0 \u0026\u0026 y%100!=0) || y%400==0){ // 闰年 if(m\u003e=1 \u0026\u0026 m\u003c=12 \u0026\u0026 d\u003e=1 \u0026\u0026 d\u003c=mon1[m]) return true; } else{ // 非闰年 if(m\u003e=1 \u0026\u0026 m\u003c=12 \u0026\u0026 d\u003e=1 \u0026\u0026 d\u003c=mon2[m]) return true; } return false; } int main() { int date,fh=0,ye,mo,da; cin\u003e\u003edate; int a=date/10000000; int b=date/1000000%10; int c=date/100000%100; int d=date/10000%1000; // 寻找第一个回文串日期 int flag=0; for(int i=a;i\u003c=9;i++){ for(int j=0;j\u003c=3;j++){ for(int k=0;k\u003c=9;k++){ if(i*10000001+j*1000010+k*100100 + 11000 \u003c= date) continue; if(i*10000001+j*1000010+k*100100 \u003e date){ // d=0 if(isDate(i*1000+j*100+k*10, k, j*10+i)){ cout \u003c\u003c i*10000001+j*1000010+k*100100 \u003c\u003cendl; flag=1; break; } } if(isDate(i*1000+j*100+k*10+1, 10+k, j*10+i)){ // d=1 cout \u003c\u003c i*10000001+j*1000010+k*100100+11000 \u003c\u003cendl; flag=1; break; } } if(flag==1) break; } if(flag==1) break; } // 寻找第一个 ABABBABA型回文串 if(a*10000000+a*100000+a*100+a\u003edate) cout\u003c\u003c a*10000000+a*100000+a*100+a \u003c\u003cendl; else if(a*10000000+a*100000+a*100+a+1011010\u003edate \u0026\u0026 (a==1 || a==2)) cout\u003c\u003c a*10000000+a*100000+a*100+a+1011010 \u003c\u003cendl; else cout\u003c\u003c (a+1)*10000000+(a+1)*100000+(a+1)*100+a+1\u003c\u003cendl; return 0; } ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:5:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["labuladong的算法秘籍"],"content":"1 题目 实现一个计算器,功能如下: 1、输入一个字符串,可以包含 + - * /、数字、括号以及空格,你的算法返回运算结果。 2、要符合运算法则,括号的优先级最高,先乘除后加减。 3、除号是整数除法,无论正负都向 0 取整(5/2=2,-5/2=-2)。 4、可以假定输入的算式⼀定合法,且计算过程不会出现整型溢出,不会出现除数为 0 的意外情况。 ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/:1:0","tags":["labuladong","数据结构"],"title":"【数据结构设计】实现一个计算器","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/"},{"categories":["labuladong的算法秘籍"],"content":"2 解析 ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/:2:0","tags":["labuladong","数据结构"],"title":"【数据结构设计】实现一个计算器","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/"},{"categories":["labuladong的算法秘籍"],"content":"2.1 字符串转整数 // 把字符串s转为整数n int n = 0; for (int i = 0; i \u003c s.size(); i++) { char c = s[i]; n = 10 * n + (c - '0'); } ❗❗❗ 注意 (c - ‘0’) 的括号不能省略,否则可能造成整型溢出。 ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/:2:1","tags":["labuladong","数据结构"],"title":"【数据结构设计】实现一个计算器","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/"},{"categories":["labuladong的算法秘籍"],"content":"2.2 处理加减法 🟠 先给第⼀个数字加⼀个默认符号 +,变成 +1-12+3。 🟡 把⼀个运算符和数字组合成⼀对,也就是三对 +1,-12,+3,把它们转化成数字,然后放到⼀个栈 中。 🟢 将栈中所有的数字求和,就是原算式的结果。 int calculate(string s) { stack\u003cint\u003e stk; // 记录算式中的数字 int num = 0; // 记录 num 前的符号,初始化为 + char sign = '+'; for (int i = 0; i \u003c s.size(); i++) { char c = s[i]; // 如果是数字,连续读取到 num if (isdigit(c)) num = 10 * num + (c - '0'); // 如果不是数字,就是遇到了下⼀个符号, // 之前的数字和符号就要存进栈中 if (!isdigit(c) || i == s.size() - 1) { switch (sign) { case '+': stk.push(num); break; case '-': stk.push(-num); break; } // 更新符号为当前符号,数字清零 sign = c; num = 0; } } // 将栈中所有结果求和就是答案 int res = 0; while (!stk.empty()) { res += stk.top(); stk.pop(); } return res; } ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/:2:2","tags":["labuladong","数据结构"],"title":"【数据结构设计】实现一个计算器","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/"},{"categories":["labuladong的算法秘籍"],"content":"2.3 处理乘除法 思路跟仅处理加减法类似,其他部分都不变,只需要在 switch 部分加上对应的 case 。 for (int i = 0; i \u003c s.size(); i++) { char c = s[i]; if (isdigit(c)) num = 10 * num + (c - '0'); if (!isdigit(c) || i == s.size() - 1) { switch (sign) { int pre; case '+': stk.push(num); break; case '-': stk.push(-num); break; // 只要拿出前⼀个数字做对应运算即可 case '*': pre = stk.top(); stk.pop(); stk.push(pre * num); break; case '/': pre = stk.top(); stk.pop(); stk.push(pre / num); break; } // 更新符号为当前符号,数字清零 sign = c; num = 0; } } 当遇到空格时,只需要控制空格不进入 if 条件: if ((!isdigit(c) \u0026\u0026 c != ' ') || i == s.size() - 1) { ... } ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/:2:3","tags":["labuladong","数据结构"],"title":"【数据结构设计】实现一个计算器","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/"},{"categories":["labuladong的算法秘籍"],"content":"2.4 处理括号 💥 括号具有递归性质。 我们只需在遍历时,遇到 ( 开始递归,遇到 ) 结束递归。 def calculate(s: str) -\u003e int: def helper(s: List) -\u003e int: stack = [] sign = '+' num = 0 while len(s) \u003e 0: c = s.popleft() if c.isdigit(): num = 10 * num + int(c) # 遇到左括号开始递归计算 num if c == '(': num = helper(s) if (not c.isdigit() and c != ' ') or len(s) == 0: if sign == '+': stack.append(num) elif sign == '-': stack.append(-num) elif sign == '*': stack[-1] = stack[-1] * num elif sign == '/': # python 除法向 0 取整的写法 stack[-1] = int(stack[-1] / float(num)) num = 0 sign = c # 遇到右括号返回递归结果 if c == ')': break return sum(stack) return helper(collections.deque(s)) ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/:2:4","tags":["labuladong","数据结构"],"title":"【数据结构设计】实现一个计算器","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/"},{"categories":["C++"],"content":"1 简介 🟠 vector 容器实现的是一个动态数组,即可以进行元素的插入和删除,并动态调整所占用的内存空间,整个过程无需人工干预。 🟡 在尾部插入/删除元素时,时间复杂度为O(1);在头部或者中部插入/删除元素时,时间复杂度为O(n)。 🔵 使用需加上头文件:#include \u003cvector\u003e ","date":"2023-03-16","objectID":"/stlvector%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:1:0","tags":["C++","STL","vector"],"title":"【STL】vector容器用法","uri":"/stlvector%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"2 创建vector容器 1️⃣ 调用 vector 容器类的默认构造函数。(若默认指定了 std 命令空间,则 std:: 可省略) std::vector\u003cint\u003e v1; 2️⃣ 在创建 vector 容器的同时,进行初始化。 std::vector\u003cint\u003e v1 {2, 3, 5, 7, 11, 13, 17, 19}; 3️⃣ 在创建 vector 容器时,指定元素个数。 v1 容器开始时就有 20 个元素,它们的默认初始值都为 0。圆括号中的2个参数既可以是常量,也可以是变量。 std::vector\u003cint\u003e v1(20, 0); 4️⃣ 通过迭代器,取已建 vector 容器中指定区域内的键值对,创建并初始化新的 vector 容器。 int array[]={1,2,3}; std::vector\u003cint\u003ev1 (array, array+2); //v1 将保存{1,2} std::vector\u003cint\u003ev1 {1,2,3,4,5}; std::vector\u003cint\u003ev2 (std::begin(v1),std::begin(v1)+3); //v2保存{1,2,3} ","date":"2023-03-16","objectID":"/stlvector%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:2:0","tags":["C++","STL","vector"],"title":"【STL】vector容器用法","uri":"/stlvector%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"3 常用的成员方法 成员方法 功能 begin() 返回指向容器中第一个元素的迭代器。 end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。 size() 返回实际元素个数。 capacity() 返回当前容量。 empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。 reserve() 增加容器的容量。 front() 返回第一个元素的引用。 back() 返回最后一个元素的引用。 push_back() 在序列的尾部添加一个元素。 pop_back() 移出序列尾部的元素。 insert() 在指定的位置插入一个或多个元素。 erase() 移出一个元素或一段元素。 clear() 移出所有的元素,容器大小变为 0。 swap() 交换两个容器的所有元素。 emplace() 在指定的位置直接生成一个元素。 ","date":"2023-03-16","objectID":"/stlvector%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:3:0","tags":["C++","STL","vector"],"title":"【STL】vector容器用法","uri":"/stlvector%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"1 题目 力扣 380. O(1) 时间插入、删除和获取随机元素 实现RandomizedSet 类: RandomizedSet() 初始化 RandomizedSet 对象 bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false 。 bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false 。 int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。 你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1) 。 ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%B8%B8%E6%95%B0%E6%97%B6%E9%97%B4%E6%9F%A5%E6%89%BE%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0/:1:0","tags":["labuladong","数据结构"],"title":"【数据结构设计】常数时间查找数组元素","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%B8%B8%E6%95%B0%E6%97%B6%E9%97%B4%E6%9F%A5%E6%89%BE%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"2 解析 考虑题目的两个要求: 🔴 插入、删除、查询随机元素的时间复杂度必须都是 O(1)。 想到使用 STL 中的 map 结构。 🟡 getRandom() 必须等概率的返回随机元素。 那么底层必须用数组实现,且数组是紧凑的,这样就可以直接生成随机数作为数组索引。 🟢 综合考虑以上2个条件:在 O(1) 的时间删除数组中的某⼀个元素 val,可以先把这个元素交换到数组的尾部,然后再 pop 掉。而交换两个元素需要知道索引,故用哈希表存储每个元素及其索引。 代码实现: class RandomizedSet { public: // 存储元素的值 vector\u003cint\u003e ve; // 键是元素值,值是元素在ve中的索引 unordered_map\u003cint, int\u003e ma; RandomizedSet() { } bool insert(int val) { // 若 val 不存在,则插入并返回true if(ma.find(val) == ma.end()){ // 注意条件!别写反了 // 并记录 val 对应的索引值,注意添加键值对的写法 ma[val]=ve.size(); ve.push_back(val); return true; } return false; } bool remove(int val) { if(ma.find(val) != ma.end()){ // 将最后⼀个元素对应的索引修改为 ma[val] ma[ve.back()]=ma[val]; // 交换 val 和最后⼀个元素 swap(ve[ma[val]],ve.back()); // 在数组中删除元素 val ve.pop_back(); // 删除元素 val 对应的索引 ma.erase(val); return true; } return false; } int getRandom() { // 随机获取 nums 中的⼀个元素 return ve[rand() % ve.size()]; } }; ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%B8%B8%E6%95%B0%E6%97%B6%E9%97%B4%E6%9F%A5%E6%89%BE%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0/:2:0","tags":["labuladong","数据结构"],"title":"【数据结构设计】常数时间查找数组元素","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%B8%B8%E6%95%B0%E6%97%B6%E9%97%B4%E6%9F%A5%E6%89%BE%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0/"},{"categories":["蓝桥杯刷题记录"],"content":"1 单词分析 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:1:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 请你帮助小蓝,给了一个单词后,帮助他找到出现最多的字母和这 个字母出现的次数。 输入描述 输入一行包含一个单词,单词只由小写英文字母组成。 对于所有的评测用例,输入的单词长度不超过 1000。 输出描述 输出两行,第一行包含一个英文字母,表示单词中出现得最多的字母是哪个。如果有多个字母出现的次数相等,输出字典序最小的那个。 第二行包含一个整数,表示出现得最多的那个字母在单词中出现的次数。 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:1:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:20min 🎯难度:💡 读取输入的单个字符:while((c=getchar())!='\\n') 两字符串相减就是整数,不用 (int) 强制类型转换 在 while() 的条件里用 ++,要检查是否会出错 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:1:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 #include \u003ciostream\u003eusing namespace std; int main(){ char c; // 每次读取进来的字符 int idx=0; // 记录个数最大的下标 int a[26]={0}; // 记录每个字符的个数 while((c=getchar())!='\\n'){ a[c-'a']++; } for(int i=1;i\u003c26;i++){ if(a[i]\u003ea[idx]) idx=i; } printf(\"%c\\n%d\", 'a'+idx, a[idx]); return 0; // 最好每次加上 } ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:1:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"2 成绩统计 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:2:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 小蓝给学生们组织了一场考试,卷面总分为 100 分,每个学生的得分都是一个 0 到 100 的整数。 如果得分至少是 60 分,则称为及格。如果得分至少为 85 分,则称为优秀。 请计算及格率和优秀率,用百分数表示,百分号前的部分四舍五入保留整 数。 输入描述 输入的第一行包含一个整数 n (1≤n≤$10^4$) ,表示考试人数。 接下来 n 行,每行包含一个 0 至 100 的整数,表示一个学生的得分。 输出描述 输出两行,每行一个百分数,分别表示及格率和优秀率。百分号前的部分 四舍五入保留整数。 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:2:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:20min 🎯难度:💡 注意 double d 四舍五入的写法:int i = d + 0.5 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:2:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 #include \u003ciostream\u003eusing namespace std; int main(){ int n,s,j=0,y=0; scanf(\"%d\",\u0026n); for(int i=0;i\u003cn;i++){ scanf(\"%d\",\u0026s); if(s\u003e=85) y++,j++; else if(s\u003e=60) j++; } printf(\"%d%\\n\",(int)((double)j/(double)n*100+0.5)); printf(\"%d%\\n\",(int)((double)y/(double)n*100+0.5)); } ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:2:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"3 门牌制作 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:3:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 小蓝要为一条街的住户制作门牌号。 这条街一共有 20202020 位住户,门牌号从 11 到 20202020 编号。 小蓝制作门牌的方法是先制作 00 到 99 这几个数字字符,最后根据需要将字符粘贴到门牌上,例如门牌 1017 需要依次粘贴字符 1、0、1、71、0、1、7,即需要 11 个字符 00,22 个字符 11,11 个字符 77。 请问要制作所有的 11 到 20202020 号门牌,总共需要多少个字符 22? ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:3:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:20min 🎯难度:💡 注意从获得一个数各个位上的数字的做法:先 ‘/’ 再 ‘%’ ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:3:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 #include \u003ciostream\u003eusing namespace std; int main(){ int s=0; for(int i=1;i\u003c=2020;i++){ if(i%10==2) s++; // 个位为2 if(i/10%10==2) s++; // 十位为2 if(i/100%10==2) s++; // 百位为2 if(i/1000==2) s++; // 千位为2 } printf(\"%d\",s); return 0; } ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:3:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"4 成绩分析 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:4:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 小蓝给学生们组织了一场考试,卷面总分为 100 分,每个学生的得分都是一个 0 到 100 的整数。 请计算这次考试的最高分、最低分和平均分。 输入描述 输入的第一行包含一个整数 n (1≤n≤$10^4$) ,表示考试人数。 接下来 n 行,每行包含一个 0 至 100 的整数,表示一个学生的得分。 输出描述 输出三行。 第一行包含一个整数,表示最高分。 第二行包含一个整数,表示最低分。 第三行包含一个实数,四舍五入保留正好两位小数,表示平均分。 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:4:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:6min 🎯难度:💡 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:4:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 #include \u003ciostream\u003eusing namespace std; int main(){ int max=0,min=100,sum=0,n,s; scanf(\"%d\",\u0026n); for(int i=0;i\u003cn;i++){ scanf(\"%d\",\u0026s); if(s\u003emax) max=s; if(s\u003cmin) min=s; sum+=s; } double avg=(int)(sum*100.0/n+0.5); avg/=100; printf(\"%d\\n%d\\n%.2f\",max,min,avg); return 0; } ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:4:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"5 排序 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:5:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 小蓝发现,如果对一个字符串中的字符排序,只允许交换相邻的两个字符, 则在所有可能的排序方案中,冒泡排序的总交换次数是最少的。 例如,对于字符串 lan 排序,只需要 11 次交换。对于字符串 qiao 排序,总共需要 44 次交换。 小蓝找到了很多字符串试图排序,他恰巧碰到一个字符串,需要 100 次交 换,可是他忘了把这个字符串记下来,现在找不到了。 请帮助小蓝找一个只包含小写英文字母且没有字母重复出现的字符串,对 该串的字符排序,正好需要 100 次交换。如果可能找到多个,请告诉小蓝最短的那个。如果最短的仍然有多个,请告诉小蓝字典序最小的那个。 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:5:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"答题情况 ⏰解题耗时:30min 🎯难度:💡💡 需要熟练掌握【冒泡排序】 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:5:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 首先需要回顾一下冒泡排序的基本内容。 🎨 冒泡排序流程 ① 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 ② 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。 ③ 针对所有的元素重复以上的步骤,除了最后已经选出的元素(有序)。 💦 冒泡排序代码 void bubble_sort(int arr[], int len) { int i, j; for (i = 0; i \u003c len - 1; i++) for (j = 0; j \u003c len - 1 - i; j++) if (arr[j] \u003e arr[j + 1]) swap(arr[j], arr[j + 1]); } 当拥有 n 个字符的字符串完全倒序时,交换次数最多,需要交换 n(n-1)/2 次。 而题目中需要交换 100 次,因此字符串长度至少为15。此时 15*14/2 = 105,最多交换 105 次,字符串为 “onmlkjihgfedcba” 。而结果字符串的字典顺序要小,因此把 j 提到最前面,交换次数减为 100,且此时字典顺序最小。 最终答案代码: #include \u003ciostream\u003eusing namespace std; int main() { // 请在此输入您的代码 cout\u003c\u003c\"jonmlkihgfedcba\"\u003c\u003cendl; return 0; } ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:5:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["CSP刷题记录"],"content":"1 // 需要用 CPP11 或 CPP14 // #include\u003cbits/stdc++.h\u003e #include \u003ciostream\u003eusing namespace std; int main(){ // 输入 n,m int n,m; scanf(\"%d %d\",\u0026n,\u0026m); // 输入数列 int a[n+1]={0}; for(int i=1;i\u003c=n;i++){ scanf(\"%d\",\u0026a[i]); } // 计算 int sum=0; int t=0; for(int i=1;i\u003cm;i++){ if(i\u003e=a[n]){ sum=sum+(m-i)*n; break; } for(int j=t+1;j\u003c=n;j++){ if(a[j]\u003ei){ t=j-1; sum=sum+t; break; } } } printf(\"%d\", sum); } ","date":"2023-03-15","objectID":"/csp202112/:1:0","tags":["CSP","C++"],"title":"【CSP】202112题解","uri":"/csp202112/"},{"categories":["CSP刷题记录"],"content":"2 错误的: ","date":"2023-03-15","objectID":"/csp202112/:2:0","tags":["CSP","C++"],"title":"【CSP】202112题解","uri":"/csp202112/"},{"categories":["CSP刷题记录"],"content":"3 #include \u003cbits/stdc++.h\u003eusing namespace std; long long w,s,f; string x; vector\u003clong long\u003ev; int main(){ cin\u003e\u003ew\u003e\u003es\u003e\u003ex; // 输入所有:w 每行能容纳的码字数,s 校验级别,x 字符串 long long tp=0; // 记录上一个字符的类型:0 大写字母,1 小写字母,2 数字 for(long long i=0;i\u003cx.length();i++){ if(x[i]\u003e='A'\u0026\u0026x[i]\u003c='Z'){ // 这个是大写字母 if(tp==1){ // 小→大:28 28 v.push_back(28); v.push_back(28); } else if(tp==2){ // 数→大:28 v.push_back(28); } tp=0; // 更新上一个字符类型为 0 v.push_back(x[i]-'A'); // 加入字符 } else if(x[i]\u003e='a'\u0026\u0026x[i]\u003c='z'){ // 这个是小写字母 if(tp==0||tp==2)v.push_back(27); // 大→小、数→小:27 v.push_back(x[i]-'a'); // 加入字符 tp=1; // 更新上一个字符类型为 1 } else { // 这个是数字 if(tp!=2)v.push_back(28); // 大→数、小→数:28 tp=2 // 更新上一个字符类型为 2 v.push_back(x[i]-'0'); // 加入字符 } } if(v.size()%2){ // 如果数字有奇数个,就补齐 v.push_back(29); } vector\u003clong long\u003ev2; // 存储码字 for(long long i=0;i\u003cv.size();i+=2){ // 计算码字 v2.push_back(v[i]*30+v[i+1]); } long long len=v2.size()+1; // 数据区码字个数=1(码字长度)+数据码字个数 if(s==-1){ while(len%w){ // 用 900 补齐 v2.push_back(900); ++len; } cout\u003c\u003clen\u003c\u003cendl; // 第一个是码字的数量 for(auto x:v2)cout\u003c\u003cx\u003c\u003cendl; return 0; } while((len+(1\u003c\u003c(s+1)))%w){ // 用 1\u003c\u003c(s+1) 计算校验码字的数目 ++len; v2.push_back(900); } vector\u003clong long\u003ev3; // v3 存储 dn 的系数,按高位到低位 v3.push_back(len); for(auto X:v2){ v3.push_back(X); } for(auto x:v3)printf(\"%d\\n\",x); // 输出码字长度+数据码字个数 vector\u003clong long\u003ev4; // 存储 g(x) 的系数,按高位到低位,最高 k 次 v4.push_back(1); v4.push_back(-3); long long mul=-3; for(long long i=2;i\u003c=(1\u003c\u003c(s+1));i++){ // 计算 g(x) 的系数 v4.push_back(0); mul=mul*3%929; // x 的 i 次方 for(long long j=v4.size()-1;j\u003e=1;j--) v4[j]+=v4[j-1]*mul%929,v4[j]%=929; } long long w4=1\u003c\u003c(s+1); // k for(long long i=1;i\u003c=w4;i++) v3.push_back(0); // 把 v3 变成 x 的 k+n-1 次方的系数 for(long long i=0;i\u003cv3.size();i++){ // i:相除时上的 x 的最高位 long long num=v3[i]; // x 的 i 次方的系数,除数 × 被除数最高位系数 for(long long j=i+1;j\u003c=i+w4;j++) // 相减时,最高位相同不用减,所以 j 从 i+1 开始,而 g(x) 除了最高位,最多还有 k 项 v3[j]-=num*v4[j-i]%929,v3[j]%=929; // 被除数最高位需要对齐的系数也在变化 if(i==v3.size()-1-w4){ f=i+1; break; } // } while(f\u003cv3.size()){ long long ans1=(-v3[f])%929; if(ans1\u003c0)ans1+=929; cout\u003c\u003cans1\u003c\u003cendl; ++f; } return 0; } #include \u003cbits/stdc++.h\u003eusing namespace std; int main(){ // 1. 输入数据 int w,s; // 每行能容纳的码字数、校验级别 string str; // 输入的非空字符串 vector\u003cint\u003e v1; // 最初的数字序列 cin\u003e\u003ew\u003e\u003es\u003e\u003estr; int k=(s==-1 ? 0 : (1\u003c\u003c(s+1))); // 2. 产生数字序列 int flag=-1; // 表示一个数属于哪一类:0 大写,1 小写,2 数字 for(int i=0;i\u003cstr.length();i++){ if(str[i]\u003e='A' \u0026\u0026 str[i]\u003c='Z'){ // 大写 if(flag==1){ // 前一个是小写 v1.push_back(28); v1.push_back(28); } else if(flag==2) // 前一个是数字 v1.push_back(28); v1.push_back(str[i]-'A'); flag=0; } else if(str[i]\u003e='a' \u0026\u0026 str[i]\u003c='z'){ // 小写 if(flag==0 || flag==2) // 前一个是大写或数字 v1.push_back(27); v1.push_back(str[i]-'a'); flag=1; } else if(str[i]\u003e='0' \u0026\u0026 str[i]\u003c='9'){ // 数字 if(flag==0 || flag==1) // 前一个是大写或数字 v1.push_back(28); v1.push_back(str[i]-'0'); flag=2; } } // 3. 计算有效数据码字 if(v1.size()%2) // 只有奇数个字 v1.push_back(29); vector\u003clong long\u003e v2; // 存储有效数据的码字 v2.push_back(0); // 第一个放数据码字的总个数,先占个坑位 for(int i=0;i\u003cv1.size();i+=2){ // 加入有效数据 v2.push_back(30*v1[i]+v1[i+1]); } while((v2.size()+k)%w) // 填充数据 v2.push_back(900); v2[0]=v2.size(); // 更新数据码字的总个数 // 4. 输出 数据码字 v2,s=-1 时,直接退出 for(int i=0;i\u003cv2.size();i++) cout\u003c\u003cv2[i]\u003c\u003cendl; if(s==-1) return 0; // 5. 计算 g(x) 中每项的系数 vector\u003clong long\u003e v3; // v3:存储 x 的系数,由高次到低次 v3.push_back(1); v3.push_back(-3); long long t=-3; for(int i=2;i\u003c=k;i++){ t=(t*3)%929; v3.push_back(0); for(int j=v3.size()-1;j\u003e=1;j--) v3[j]=(v3[j]+t*v3[j-1])%929; } // 6. 计算 d(x) 中每项的系数,存储在 v2 中 for(int i=0;i\u003ck;i++) v2.push_back(0); // 7. 计算余数 r(x) int a; for(int i=0;i\u003cv2.size();i++){ for(int j=i+1;j\u003c=i+k;j++) v2[j]=(v2[j]-v2[i]*v3[j-i])%929; if(i==v2.size()-1-k){ a=i+1; break; } } // 8. 输出最终结果 while(a\u003cv2.size()){ if((-v2[a])%929\u003c0) cout\u003c\u003c(-v2[a])%929+929\u003c\u003cendl; else cout\u003c\u003c(-v2[a])%929\u003c\u003cendl; a++; } return 0; } ","date":"2023-03-15","objectID":"/csp202112/:3:0","tags":["CSP","C++"],"title":"【CSP】202112题解","uri":"/csp202112/"},{"categories":["CSP刷题记录"],"content":"1 错误的: #include \u003ciostream\u003e#include \u003ccmath\u003e// #define _USE_MATH_DEFINES using namespace std; int main() { // 输入变量个数 n,语句个数 k int n,k; scanf(\"%d\",\u0026n); scanf(\"%d\",\u0026k); // 输入语句 int x[n+1]={0},y[n+1]={0},a[n+1]={0}; for(int i=1;i\u003c=k;i++){ scanf(\"%d\",\u0026x[i]); scanf(\"%d\",\u0026y[i]); } for(int i=1;i\u003c=k;i++){ if(a[x[i]]==0){ a[x[i]]=i; } } // 记录不符合的语句个数 int cnt=0; for(int i=1;i\u003c=k;i++){ if(y[i]!=0 \u0026\u0026 i\u003c=a[y[i]]){ cnt++; } } printf(\"%d\",cnt); } 正确的: // 需要用 CPP11 或 CPP14 // #include\u003cbits/stdc++.h\u003e #include \u003ciostream\u003eusing namespace std; int main(){ // 输入变量数量 n,赋值语句 k int n,k; scanf(\"%d %d\",\u0026n,\u0026k); // 用 cin 可能超时,改为 scanf // 输入 k 行语句 x[i] y[i] int x[k],y[k]; for(int i=0;i\u003ck;i++){ scanf(\"%d %d\",\u0026x[i],\u0026y[i]); } // 判断有无不符合赋值规则的 int cnt=0; for(int i=0;i\u003ck;i++){ int flag=1; if(y[i]==0) continue; for(int j=0;j\u003ci;j++){ if(x[j]==y[i]){ flag=0; break; } } cnt += flag; } cout\u003c\u003ccnt; } ","date":"2023-03-14","objectID":"/csp202203/:1:0","tags":["CSP","C++"],"title":"【CSP】202203题解","uri":"/csp202203/"},{"categories":["CSP刷题记录"],"content":"2 // 需要用 CPP11 或 CPP14 // #include\u003cbits/stdc++.h\u003e #include \u003ciostream\u003eusing namespace std; int main(){ // 输入计划数 n,查询数 m,等核酸天数 k int n,m,k; scanf(\"%d %d %d\",\u0026n,\u0026m,\u0026k); // 输入 t[i] c[i] int t,c; // 错误原因:1. 数组开的不够大 2. 数组没有初始化 int r[200005]={0}; for(int i=0;i\u003cn;i++){ scanf(\"%d %d\",\u0026t,\u0026c); if(t-k\u003c1) continue; int temp=max(1,t-k-c+1); for(int j=temp;j\u003c=t-k;j++){ r[j]++; } } // 输入 m 个 q[i] int q; for(int i=0;i\u003cm;i++){ int cnt=0; scanf(\"%d\",\u0026q); printf(\"%d\\n\",r[q]); } } ","date":"2023-03-14","objectID":"/csp202203/:2:0","tags":["CSP","C++"],"title":"【CSP】202203题解","uri":"/csp202203/"},{"categories":["CSP刷题记录"],"content":"1 #include \u003ciostream\u003e#include \u003ccmath\u003e// #define _USE_MATH_DEFINES using namespace std; int main() { // 输入 n,a,计算总和 sum int n,sum=0; scanf(\"%d\",\u0026n); int a[n]; for(int i=0;i\u003cn;i++){ scanf(\"%d\",\u0026a[i]); sum+=a[i]; } // 计算平均数 x double x=(double)sum/n; // 计算方差 d double d=0; for(int i=0;i\u003cn;i++){ d=d+(a[i]-x)*(a[i]-x); } d=d/n; // 计算 f double f[n]; for(int i=0;i\u003cn;i++){ f[i]=(a[i]-x)/pow(d,0.5); printf(\"%.16f\\n\",f[i]); } } ","date":"2023-03-14","objectID":"/csp202206/:1:0","tags":["CSP","C++"],"title":"【CSP】202206题解","uri":"/csp202206/"},{"categories":["CSP刷题记录"],"content":"2 #include \u003ciostream\u003e#include \u003ccmath\u003e using namespace std; int main() { // 输入变量 int n,l,s; scanf(\"%d %d %d\",\u0026n,\u0026l,\u0026s); int b[s+1][s+1]; // 输入树的位置 int x[n],y[n]; for(int i=0;i\u003cn;i++){ scanf(\"%d %d\",\u0026x[i],\u0026y[i]); } // 输入矩阵 S for(int i=s;i\u003e=0;i--){ for(int j=0;j\u003c=s;j++){ scanf(\"%d\",\u0026b[i][j]); } } int cnt=0; for(int i=0;i\u003cn;i++){ // 考虑树为起点构成的数组不能超过矩阵 L if(x[i]+s\u003el || y[i]+s\u003el){ continue; // 救命!这里用 continue,不是 break } // 构建新的矩阵 a int a[60][60]={0}; int flag=1; for(int j=0;j\u003cn;j++){ if(x[j]\u003e=x[i] \u0026\u0026 x[j]\u003c=x[i]+s \u0026\u0026 y[j]\u003e=y[i] \u0026\u0026 y[j]\u003c=y[i]+s){ a[x[j]-x[i]][y[j]-y[i]]=1; } } for(int j=0;j\u003c=s;j++){ for(int k=0;k\u003c=s;k++){ if(a[j][k]!=b[j][k]){ flag=0; break; } } if(flag==0) break; } cnt += flag; } printf(\"%d\",cnt); } ","date":"2023-03-14","objectID":"/csp202206/:2:0","tags":["CSP","C++"],"title":"【CSP】202206题解","uri":"/csp202206/"},{"categories":["CSP刷题记录"],"content":"3 #include\u003cbits/stdc++.h\u003eusing namespace std; class role{ public: set\u003cstring\u003e opl; set\u003cstring\u003e opt; set\u003cstring\u003e opn; }; class group{ public: set\u003cstring\u003e rol; // 关联内的用户组 }; map\u003cstring,role\u003e mpr; // 角色:键为角色名,值为角色的操作名、资源种类、资源名 map\u003cstring,group\u003e mpg; // 关联:键为角色名,值为一组用户组名 class user{ public: set\u003cstring\u003e rol; // 用户所关联的角色 set\u003cstring\u003e grp; // 用户所在的用户组 // 查看操作、资源种类、资源名是否能被用户使用 bool check(string opl,string opt,string opn){ // 遍历用户所关联的角色 for(auto it:rol) if(mpr[it].opl.count(\"*\") || mpr[it].opl.count(opl)) // 操作是否符合 if(mpr[it].opt.count(\"*\") || mpr[it].opt.count(opt)) // 资源种类是否符合 if(mpr[it].opn.empty() || mpr[it].opn.count(opn)) // 资源名是否符合 return true; // 遍历用户所在的用户组所关联的角色 for(auto it:grp) if(mpg.count(it)) // 已关联的用户是否包含用户所在的用户组 for(auto it1:mpg[it].rol) // 遍历用户组所关联的角色 if(mpr[it1].opl.count(\"*\") || mpr[it1].opl.count(opl)) if(mpr[it1].opt.count(\"*\") || mpr[it1].opt.count(opt)) if(mpr[it1].opn.empty() || mpr[it1].opn.count(opn)) return true; return false; } }; map\u003cstring,user\u003e mpu; // 关联:键为角色名,值为用户名 signed main(){ //提高cin,cout的速度 ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); int n,m,q,nv,no,nn,k; string name,rname,uname,x,y,z,ch; cin\u003e\u003en\u003e\u003em\u003e\u003eq; // 输入角色数 n,角色关联数 m,操作数 q for(int i=0;i\u003cn;i++){ cin\u003e\u003ename\u003e\u003env; // 可以直接用 cin,不用标准输入 while(nv--) cin\u003e\u003ex,mpr[name].opl.emplace(x); // 输入操作名 cin\u003e\u003eno; while(no--) cin\u003e\u003ex,mpr[name].opt.emplace(x); // 输入资源种类 cin\u003e\u003enn; while(nn--) cin\u003e\u003ex,mpr[name].opn.emplace(x); // 输入资源名 } for(int i=0;i\u003cm;i++){ cin\u003e\u003ername\u003e\u003ek; for(int j=0;j\u003ck;j++){ cin\u003e\u003ech\u003e\u003ename; if(ch == \"g\") mpg[name].rol.emplace(rname); // 在【用户组】中加入关联:角色名+用户组名 else mpu[name].rol.emplace(rname); // 在【用户】中加入关联:角色名+用户名 } } for(int i=0;i\u003cq;i++){ cin\u003e\u003euname\u003e\u003ek; for(int j=0;j\u003ck;j++) cin\u003e\u003ename,mpu[uname].grp.emplace(name); // 输入用户所在的用户组 cin\u003e\u003ex\u003e\u003ey\u003e\u003ez; cout\u003c\u003cmpu[uname].check(x,y,z)\u003c\u003cendl; mpu[uname].grp.clear(); // 每次要清空用户所在的用户组,role 不用清空!!! } } ","date":"2023-03-14","objectID":"/csp202206/:3:0","tags":["CSP","C++"],"title":"【CSP】202206题解","uri":"/csp202206/"},{"categories":["CSP刷题记录"],"content":"2 错误的: #include \u003ciostream\u003e#include \u003ccmath\u003e// #define _USE_MATH_DEFINES using namespace std; int main() { // 输入书本数量 n,包邮条件 x int n,x; scanf(\"%d\",\u0026n); scanf(\"%d\",\u0026x); // 输入书本价格,计算总和 sum int a[n]; int sum=0; for(int i=0;i\u003cn;i++){ scanf(\"%d\",\u0026a[i]); sum+=a[i]; } // 动态规划,找前 i 本书的价格和最大,不超过 sum-x int d[n][sum-x+1]={0}; for(int j=0;j\u003csum-x+1;j++){ d[0][j]=a[0]\u003ej ? 0 : a[0]; } for(int i=1;i\u003cn;i++){ for(int j=0;j\u003csum-x+1;j++){ if(d[i-1][j-a[i]]+a[i] \u003c=j){ d[i][j]=max(d[i-1][j-a[i]]+a[i],d[i-1][j]); } else d[i][j]=d[i-1][j]; } } printf(\"\\n%d\",sum-d[n-1][sum-x]); } 正确的: #include \u003ciostream\u003e#include \u003ccmath\u003e// #define _USE_MATH_DEFINES using namespace std; int main() { // 输入书本数量 n,包邮条件 x int n,x; scanf(\"%d\",\u0026n); scanf(\"%d\",\u0026x); // 输入书本价格,计算总和 sum int a[n]; int sum=0; for(int i=0;i\u003cn;i++){ scanf(\"%d\",\u0026a[i]); sum+=a[i]; } // 动态规划,找前 i 本书的价格和最大,不超过 sum-x int d[n][sum-x+1]={0}; for(int j=0;j\u003csum-x+1;j++){ d[0][j]=a[0]\u003ej ? 0 : a[0]; } for(int i=1;i\u003cn;i++){ for(int j=0;j\u003csum-x+1;j++){ if(a[i] \u003c=j){ d[i][j]=max(d[i-1][j-a[i]]+a[i],d[i-1][j]); } else d[i][j]=d[i-1][j]; } } printf(\"%d\",sum-d[n-1][sum-x]); } ","date":"2023-03-14","objectID":"/csp202209/:1:0","tags":["CSP","C++"],"title":"【CSP】202209题解","uri":"/csp202209/"},{"categories":["CSP刷题记录"],"content":"3 #include \u003cbits/stdc++.h\u003e using namespace std; struct Node{ // 一条漫游数据 int d; int u; int r; }; int n; map\u003cpair\u003cint,int\u003e,bool\u003e mp; // 键:地点+日期,值:是否是风险地区 vector\u003cNode\u003e user; // 存放有风险的用户漫游数据 int main() { ios::sync_with_stdio(false);cin.tie(0);cout.tie(0); cin \u003e\u003e n; // 输入天数 n for(int i=0;i\u003cn;i++){ int ri,mi; cin \u003e\u003e ri \u003e\u003e mi; // 输入每天的风险地数量、漫游数据量 // 先clear // map\u003cpair\u003cint,int\u003e,bool\u003e tmp; for(map\u003cpair\u003cint,int\u003e,bool\u003e::iterator iter = mp.begin();iter!=mp.end();){ map\u003cpair\u003cint,int\u003e,bool\u003e::iterator it1 = iter; iter++; if(i - ((it1-\u003efirst).second) \u003e= 7) mp.erase(it1); // 每次新的一天,检查是否有 7 天外的风险地区数据,有就删掉 } // 插入合法的 for(int j=1;j\u003c=ri;j++){ // 插入风险地区 int pij; cin \u003e\u003e pij; for(int date=i;date\u003ci+7;date++){ mp[{pij,date}] = 1; // 根据地区、时间标记风险地区 } } // 筛选不合法的 // 以前的风险用户 vector\u003cNode\u003e vc; for(auto t:user){ // 遍历漫游数据 bool f = 1; int r = t.r; if(i - t.d \u003e= 7) continue; // 只看近 7 天的数据 for(int date = t.d;date\u003c=i;date++){ if(!mp.count({r,date})){ // 该地区和日期在风险范围内 f = 0; break; } } if(f) vc.push_back(t); } user.clear(); user = vc; // 每一天都要更新风险用户列表 // 必须是七天以内收到的漫游数据 // 今天的风险用户 for(int j=1;j\u003c=mi;j++){ int d,u,r; cin \u003e\u003e d \u003e\u003e u \u003e\u003e r; if(i - d \u003e= 7) continue; // 7 天外的数据就丢掉 // check 合法性 // 从 d 到 i 天地区r均为风险地区即可 bool f = 1; for(int k=d;k\u003c=i;k++){ if(!mp.count({r,k})){ // 在风险记录内,就加进来 f = 0; break; } } if(f) user.push_back({d,u,r}); } set\u003cint\u003e ans; for(auto x:user) ans.insert(x.u); // 只取用户 cout \u003c\u003c i \u003c\u003c ' '; for(auto x:ans){ cout \u003c\u003c x \u003c\u003c ' '; } cout \u003c\u003c '\\n'; } return 0; } ","date":"2023-03-14","objectID":"/csp202209/:2:0","tags":["CSP","C++"],"title":"【CSP】202209题解","uri":"/csp202209/"},{"categories":["CSP刷题记录"],"content":"1 错误的 #include \u003ciostream\u003eusing namespace std; int main() { // 输入年数n,年利率i int n; double i; scanf(\"%d %lf\", \u0026n,\u0026i); double s=0; // 保存最后结果 int a; // 每年收益 for(int j=0;j\u003cn+1;j++){ scanf(\"%d\", \u0026a); s=s/(1+i) + a; } printf(\"%f\", s); } 正确的 #include \u003ciostream\u003eusing namespace std; int main() { // 输入年数n,年利率i int n; double i; scanf(\"%d\", \u0026n); scanf(\"%lf\",\u0026i); double s=0; // 保存最后结果 int a[n+1]; // 每年收益 for(int j=0;j\u003cn+1;j++){ scanf(\"%d\", \u0026a[j]); } for(int j=n;j\u003e=0;j--){ s=s/(1+i) + a[j]; } printf(\"%f\", s); } ","date":"2023-03-14","objectID":"/csp202212/:1:0","tags":["CSP","C++"],"title":"【CSP】202212题解","uri":"/csp202212/"},{"categories":["CSP刷题记录"],"content":"2 正确代码 #include \u003ciostream\u003eusing namespace std; int main() { // 输入天数n 科目数m int n,m; scanf(\"%d\",\u0026n); scanf(\"%d\",\u0026m); // 输入依赖科目p[i] int p[m+1]={0},t[m+1]={0}; for(int i=1;i\u003c=m;i++){ scanf(\"%d\",\u0026p[i]); } // 输入科目所需天数t[i] for(int i=1;i\u003c=m;i++){ scanf(\"%d\",\u0026t[i]); } // 计算最早开始天数 int flag=0; int beg[m+1]={0},end[m+1]={0}; for(int i=1;i\u003c=m;i++){ if(p[i]==0){ beg[i]=1; end[i]=t[i]; } else{ beg[i]=end[p[i]]+1; end[i]=beg[i]+t[i]-1; } if(end[i]\u003en){ flag=1; } } for(int i=1;i\u003c=m;i++) printf(\"%d \",beg[i]); // 计算最晚开始天数 if(flag==1) { return 0; } else{ // 存储当前科目被哪个科目依赖 int last[m+1]={0}; for(int i=m;i\u003e0;i--){ last[i]=n+1; // 记录依赖当前科目的科目中,最早的【最晚开始时间】 for(int j=i+1;j\u003c=m;j++){ if(p[j]==i){ last[i]=min(last[i],last[j]); } } last[i]=last[i]-t[i]; } printf(\"\\n\"); for(int i=1;i\u003c=m;i++) printf(\"%d \",last[i]); } } ","date":"2023-03-14","objectID":"/csp202212/:2:0","tags":["CSP","C++"],"title":"【CSP】202212题解","uri":"/csp202212/"},{"categories":["CSP刷题记录"],"content":"3 错误代码(仍然不知道哪里错了 #include \u003ciostream\u003e#include \u003ccmath\u003e// #define _USE_MATH_DEFINES using namespace std; int main() { // 输入量化矩阵 Q int Q[8][8]; for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ scanf(\"%d\",\u0026Q[i][j]); } } // 输入扫描个数 n,任务 T int n,T; scanf(\"%d\",\u0026n); scanf(\"%d\",\u0026T); // 输入一组扫描数据 int d[n]; for(int i=0;i\u003cn;i++){ scanf(\"%d\",\u0026d[i]); } // 将扫描数据放入填充矩阵 M 中 int M[8][8]={0}; for(int i=1,con=0;con\u003cn;i++){ if(i%2==1){ for(int j=i-1;j\u003e=0 \u0026\u0026 con\u003cn;j--){ M[j][i-1-j]=d[con]; con++; } } else{ for(int j=0;j\u003c=i-1 \u0026\u0026 con\u003cn;j++){ M[j][i-1-j]=d[con]; con++; } } } // 输出 M 矩阵 if(T==0){ for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ printf(\"%d \", M[i][j]); } printf(\"\\n\"); } return 0; } // 计算与量化矩阵 Q 相乘后的矩阵 M for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ M[i][j]=M[i][j]*Q[i][j]; } } // 输出与量化矩阵 Q 相乘后的矩阵 M if(T==1){ for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ printf(\"%d \", M[i][j]); } printf(\"\\n\"); } return 0; } // 对 M 进行离散余弦逆变换 int MM[8][8]; for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ double s=0; for(int u=0;u\u003c8;u++){ for(int v=0;v\u003c8;v++){ double temp=cos(acos(-1)*u*(i+0.5)/8)*cos(acos(-1)*v*(j+0.5)/8)*M[u][v]; if(u==0) temp *= pow(0.5,0.5); if(v==0) temp *= pow(0.5,0.5); s+=temp; } } s=s/4; MM[i][j]=(int)(s+128.5); MM[i][j] = MM[i][j]\u003e255 ? 255 : MM[i][j]; MM[i][j] = MM[i][j]\u003c0 ? 0 : MM[i][j]; } } // 输出与量化矩阵 Q 相乘后的矩阵 M if(T==2){ for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ printf(\"%d \", MM[i][j]); } printf(\"\\n\"); } return 0; } } 正确代码: #include \u003ciostream\u003e#include \u003ccmath\u003e// #define _USE_MATH_DEFINES using namespace std; int main() { // 输入量化矩阵 Q int Q[8][8]; for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ scanf(\"%d\",\u0026Q[i][j]); } } // 输入扫描个数 n,任务 T int n,T; scanf(\"%d\",\u0026n); scanf(\"%d\",\u0026T); // 输入一组扫描数据 int d[64]={0}; for(int i=0;i\u003cn;i++){ scanf(\"%d\",\u0026d[i]); } // 将扫描数据放入填充矩阵 M 中 int M[8][8]={0}; int idx[8][8] = { {0, 1, 5, 6, 14, 15, 27, 28}, {2, 4, 7, 13, 16, 26, 29, 42}, {3, 8, 12, 17, 25, 30, 41, 43}, {9, 11, 18, 24, 31, 40, 44, 53}, {10, 19, 23, 32, 39, 45, 52, 54}, {20, 22, 33, 38, 46, 51, 55, 60}, {21, 34, 37, 47, 50, 56, 59, 61}, {35, 36, 48, 49, 57, 58, 62, 63} }; for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ M[i][j]=d[idx[i][j]]; } } // 输出 M 矩阵 if(T==0){ for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ printf(\"%d \", M[i][j]); } printf(\"\\n\"); } return 0; } // 计算与量化矩阵 Q 相乘后的矩阵 M for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ M[i][j]=M[i][j]*Q[i][j]; } } // 输出与量化矩阵 Q 相乘后的矩阵 M if(T==1){ for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ printf(\"%d \", M[i][j]); } printf(\"\\n\"); } return 0; } // 对 M 进行离散余弦逆变换 int MM[8][8]; for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ double s=0; for(int u=0;u\u003c8;u++){ for(int v=0;v\u003c8;v++){ double temp=cos(acos(-1)*u*(i+0.5)/8)*cos(acos(-1)*v*(j+0.5)/8)*M[u][v]; if(u==0) temp *= pow(0.5,0.5); if(v==0) temp *= pow(0.5,0.5); s+=temp; } } s=s/4; MM[i][j]=(int)(s+128.5); MM[i][j] = MM[i][j]\u003e255 ? 255 : MM[i][j]; MM[i][j] = MM[i][j]\u003c0 ? 0 : MM[i][j]; } } // 输出与量化矩阵 Q 相乘后的矩阵 M if(T==2){ for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ printf(\"%d \", MM[i][j]); } printf(\"\\n\"); } return 0; } } ","date":"2023-03-14","objectID":"/csp202212/:3:0","tags":["CSP","C++"],"title":"【CSP】202212题解","uri":"/csp202212/"},{"categories":["C++"],"content":"1 简介 🟠 map 容器中键值对的键和值可以是任意数据类型,包括 C++ 基本数据类型(int、double 等)、使用结构体或类自定义的类型。 🟡 容器会自动根据各键值对的键的大小,按照既定的规则进行排序。比如 std::less\u003cT\u003e、std::greater\u003cT\u003e 规则。 🟢 键的值既不能重复也不能被修改。 🔵 使用需加上头文件:#include \u003cmap\u003e ","date":"2023-03-14","objectID":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:1:0","tags":["C++","STL","map"],"title":"【STL】map容器用法","uri":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"2 创建map容器 1️⃣ 调用 map 容器类的默认构造函数。(若默认指定了 std 命令空间,则 std:: 可省略) std::map\u003cstd::string, int\u003e map1; 2️⃣ 在创建 map 容器的同时,进行初始化。 std::map\u003cstd::string, int\u003e map1 { {\"语文\",90} , {\"数学\",100} }; 3️⃣ 利用先前已创建好的 map 容器和拷贝构造函数,再创建一个新的 map 容器。 std::map\u003cstd::string, int\u003e newMap(map1); 4️⃣ 通过迭代器,取已建 map 容器中指定区域内的键值对,创建并初始化新的 map 容器。 std::map\u003cstd::string, int\u003e map1 { {\"语文\",90} , {\"数学\",100} }; std::map\u003cstd::string, int\u003e newMap(++map1.begin(), map1.end()); ","date":"2023-03-14","objectID":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:2:0","tags":["C++","STL","map"],"title":"【STL】map容器用法","uri":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"3 常用的成员方法 成员方法 功能 begin() 返回指向容器中第一个键值对的双向迭代器。(是已排好序的第一个) end() 返回指向容器最后一个元素所在位置后一个位置的双向迭代器。(是已排好序的最后一个) find(key) 在 map 容器中查找键为 key 的键值对,如果成功找到,则返回指向该键值对的双向迭代器;否则返回和 end() 方法一样的迭代器。 empty() 若容器为空,则返回 true;否则 false。 size() 返回当前 map 容器中存有键值对的个数。 insert() 向 map 容器中插入键值对。 erase() 删除 map 容器指定位置、指定键(key)值或者指定区域内的键值对。 clear() 清空 map 容器中所有的键值对,即使 map 容器的 size() 为 0。 emplace() 在当前 map 容器中的指定位置处构造新键值对。其效果和插入键值对一样,但效率更高。 count(key) 在当前 map 容器中,查找键为 key 的键值对的个数并返回。注意,由于 map 容器中各键值对的键的值是唯一的,因此该函数的返回值最大为 1。 count(key) 在容器中查找以 key 键的键值对的个数。 ","date":"2023-03-14","objectID":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:3:0","tags":["C++","STL","map"],"title":"【STL】map容器用法","uri":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"4 其他容器 ","date":"2023-03-14","objectID":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:4:0","tags":["C++","STL","map"],"title":"【STL】map容器用法","uri":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"unordered_map 容器 🟠 该容器内部不会自行对存储的键值对进行排序,其余用法和 map 类似。 🟡 使用需加上头文件: #include \u003cunordered_map\u003e ","date":"2023-03-14","objectID":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:4:1","tags":["C++","STL","map"],"title":"【STL】map容器用法","uri":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"1 简介 C++ 标准函数库中的 set 可以用来存储集合,set 里面的元素都是唯一的,不可以重复,可以新增或删除元素,但不可以修改元素的值。 🔴 头文件:#include \u003cset\u003e ","date":"2023-03-13","objectID":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:1:0","tags":["C++","STL","set"],"title":"【STL】set容器用法","uri":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"2 初始化 std::set 的初始化有三种方式:1️⃣ 以 insert() 函數新增元素 2️⃣ 直接在创建时以大括号初始化 set 内部的元素 3️⃣ 通过数组初始化。 // 第 1 种初始化方式 set\u003cint\u003e set1; set1.insert(1); set1.insert(2); set1.insert(3); // 第 2 种初始化方式 // 注意这里没有 '=' set\u003cint\u003e set2 {1,2,3}; // 第 3 种初始化方式 int num[] = {1,2,3}; set\u003cint\u003e set3(num, num+3); ","date":"2023-03-13","objectID":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:2:0","tags":["C++","STL","set"],"title":"【STL】set容器用法","uri":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"3 增/删元素 std::set 若要新增、刪除元素,可以使用 insert() 和 erase() 函数。 // 新增元素 set1.insert(1); // 删除元素 set1.erase(1); ","date":"2023-03-13","objectID":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:3:0","tags":["C++","STL","set"],"title":"【STL】set容器用法","uri":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"4 查询元素 查询 std::set 中是否包含特定的元素,可以使用 find() 函数,若成功找到指定的元素,就返回对应的 iterator,而如果没有找到,就返回 set::end。 // 在 set1 中寻找 6 这个元素 set\u003cint\u003e::iterator iter; iter = set1.find(6); // 如果找到,就返回正确的 iterator,否则返回 set1.end() if (iter != set1.end()) { cout \u003c\u003c \"Found: \" \u003c\u003c *iter \u003c\u003c endl; } ","date":"2023-03-13","objectID":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:4:0","tags":["C++","STL","set"],"title":"【STL】set容器用法","uri":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"5 列出所有元素 列出 std::set 中的所有元素有2种方式:1️⃣ 标准的 iterator 方式 2️⃣ 新的 for 遍历方法 🟠 想要拷贝元素:for(auto x:range) 🟡 想要修改元素:for(auto \u0026\u0026x:range) 🟢 想要只读元素:for(const auto\u0026 x:range) // 第 1 种列出所有元素的方法 set\u003cint\u003e::iterator iter; for(iter=set1.begin(); iter!=set1.end(); iter++){ cout \u003c\u003c *iter \u003c\u003c endl; // 注意 endl 不要写成 end } // 第 2 种列出所有元素的方法 for(const auto \u0026i : set1){ std::cout \u003c\u003c i \u003c\u003c endl; // 注意这里 i 前面没有 '*' } ","date":"2023-03-13","objectID":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:5:0","tags":["C++","STL","set"],"title":"【STL】set容器用法","uri":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"6 清空所有元素 1️⃣ 清空 std::set 中的所有元素: clear() 函数 2️⃣ 获取元素个数: size() 函数 3️⃣ 检查 std::set 是否是空的: empty() 函数。 // 清空所有元素 set1.clear(); // 获取元素个数: cout \u003c\u003c \"number of elements:\" \u003c\u003c set1.size() \u003c\u003c endl; // 检查 set 是否为空 if(set1.empty()){ cout \u003c\u003c \"set1 is empty.\" \u003c\u003c } ","date":"2023-03-13","objectID":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:6:0","tags":["C++","STL","set"],"title":"【STL】set容器用法","uri":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["OS"],"content":"1 绪论 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:1:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"2 OS的结构和硬件支持 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:2:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"3 操作系统的用户接口 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:3:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"4 进程及进程管理 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:4:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"5 资源分配与调度 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:5:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"6 处理机调度 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:6:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"7 主存管理 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:7:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"8 设备管理 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:8:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"9 文件系统 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:9:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"1 绪论 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:1:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"2 关系数据库 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:2:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"3 SQL语言 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:3:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"4 数据库安全性 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:4:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"5 数据库完整性 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:5:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"6 关系数据理论 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:6:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"7 数据库设计 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:7:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"8 关系数据库引擎基础 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:8:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"9 关系数据库查询优化 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:9:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"10 数据库恢复技术 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:10:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"11 并发控制 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:11:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["labuladong的算法秘籍"],"content":"1 题目 力扣 146. LRU 缓存 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类: LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存 int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。 void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。 ","date":"2023-01-17","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/:1:0","tags":["labuladong","数据结构","LRU"],"title":"【数据结构设计】LRU算法","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"2 解析 ","date":"2023-01-17","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/:2:0","tags":["labuladong","数据结构","LRU"],"title":"【数据结构设计】LRU算法","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"2.1 LRU算法设计 由于 put 和 get 方法的时间复杂度为 O(1),我们可以总结出 cache 这个数据结构必要的条件: 🔴 cache 中的元素必须有时序,以区分最近使用的和久未使用的数据 🟡 能在 cache 中快速找某个 key 是否已存在并得到对应的 val 🟢 cache 要支持在任意位置快速插入和删除元素,便于访问某个 key 并将这个元素变为最近使用的 哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表 LinkedHashMap。 LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。 如图所示: 根据这个结构分析以上 3 个条件: 🟥 每次从链表尾部添加元素,则尾部就是最近使用的,头部就是最久未使用的 🟨 通过哈希表可以根据 key 快速获得 val 🟩 用哈希表将 key 快速映射到链表节点后,可以很方便的进行插入、删除操作 两个问题: ❓ 用单链表取代双链表行不行? ❓ 哈希表中存了 key,链表中为什么还要存 key 和 val?可以只存 val 吗? ","date":"2023-01-17","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/:2:1","tags":["labuladong","数据结构","LRU"],"title":"【数据结构设计】LRU算法","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"2.2 代码实现 首先写出双链表节点类型 Node: class Node { public int key, val; public Node next, prev; public Node(int k, int v) { this.key = k; this.val = v; } } 再用 Node 类型构建一个双链表: class DoubleList { // 头尾虚节点 private Node head, tail; // 链表元素数 private int size; public DoubleList() { // 初始化双向链表的数据 head = new Node(0, 0); tail = new Node(0, 0); head.next = tail; tail.prev = head; size = 0; } // 在链表尾部添加节点 x,时间 O(1) public void addLast(Node x) { x.prev = tail.prev; x.next = tail; tail.prev.next = x; tail.prev = x; size++; } // 删除链表中的 x 节点(x ⼀定存在) // 由于是双链表且给的是⽬标 Node 节点,时间 O(1) public void remove(Node x) { x.prev.next = x.next; x.next.prev = x.prev; size--; } // 删除链表中第⼀个节点,并返回该节点,时间 O(1) public Node removeFirst() { if (head.next == tail) return null; Node first = head.next; remove(first); return first; } // 返回链表⻓度,时间 O(1) public int size() { return size; } } ✅ 【为什么必须要用双向链表】 因为我们需要删除操作。删除⼀个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。 有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可,先搭出代码框架: class LRUCache { // key -\u003e Node(key, val) private HashMap\u003cInteger, Node\u003e map; // Node(k1, v1) \u003c-\u003e Node(k2, v2)... private DoubleList cache; // 最⼤容量 private int cap; public LRUCache(int capacity) { this.cap = capacity; map = new HashMap\u003c\u003e(); cache = new DoubleList(); } } 一些辅助函数的实现: /* 将某个 key 提升为最近使⽤的 */ private void makeRecently(int key) { Node x = map.get(key); // 先从链表中删除这个节点 cache.remove(x); // 重新插到队尾 cache.addLast(x); } /* 添加最近使⽤的元素 */ private void addRecently(int key, int val) { Node x = new Node(key, val); // 链表尾部就是最近使⽤的元素 cache.addLast(x); // 别忘了在 map 中添加 key 的映射 map.put(key, x); } /* 删除某⼀个 key */ private void deleteKey(int key) { Node x = map.get(key); // 从链表中删除 cache.remove(x); // 从 map 中删除 map.remove(key); } /* 删除最久未使⽤的元素 */ private void removeLeastRecently() { // 链表头部的第⼀个元素就是最久未使⽤的 Node deletedNode = cache.removeFirst(); // 同时别忘了从 map 中删除它的 key int deletedKey = deletedNode.key; map.remove(deletedKey); } ✅ 【为什么要在链表中同时存储 key 和 val,而不是只存储 val】 当缓存容量已满,我们不仅仅要删除最后⼀个 Node 节点,还要把 map 中映射到该节点的 key 也删除,而 key 只能由 Node 得到。如果 Node 结构中只存储 val,那么我们就无法得知 key 是什么,也就无法删除 map 中的键。 根据以上辅助函数,可以实现 put 和 get 函数: public int get(int key) { if (!map.containsKey(key)) { return -1; } // 将该数据提升为最近使⽤的 makeRecently(key); return map.get(key).val; } public void put(int key, int val) { if (map.containsKey(key)) { // 删除旧的数据 deleteKey(key); // 新插⼊的数据为最近使⽤的数据 addRecently(key, val); return; } if (cap == cache.size()) { // 删除最久未使⽤的元素 removeLeastRecently(); } // 添加为最近使⽤的元素 addRecently(key, val); } ","date":"2023-01-17","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/:2:2","tags":["labuladong","数据结构","LRU"],"title":"【数据结构设计】LRU算法","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"3 内置类型 LinkedHashMap 也可以用 Java 的内置类型 LinkedHashMap 来实现 LRU 算法,逻辑和之前完全⼀致: class LRUCache { int cap; LinkedHashMap\u003cInteger, Integer\u003e cache = new LinkedHashMap\u003c\u003e(); public LRUCache(int capacity) { this.cap = capacity; } public int get(int key) { if (!cache.containsKey(key)) { return -1; } // 将 key 变为最近使⽤ makeRecently(key); return cache.get(key); } public void put(int key, int val) { if (cache.containsKey(key)) { // 修改 key 的值 cache.put(key, val); // 将 key 变为最近使⽤ makeRecently(key); return; } if (cache.size() \u003e= this.cap) { // 链表头部就是最久未使⽤的 key int oldestKey = cache.keySet().iterator().next(); cache.remove(oldestKey); } // 将新的 key 添加链表尾部 cache.put(key, val); } private void makeRecently(int key) { int val = cache.get(key); // 删除 key,重新插⼊到队尾 cache.remove(key); cache.put(key, val); } } 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-17","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/:3:0","tags":["labuladong","数据结构","LRU"],"title":"【数据结构设计】LRU算法","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"【单调队列】主要是为了解决以下场景: 给你一个数组 window,已知其最值为 A,如果给 window 中添加⼀个数 B,那么比较一下 A 和 B 就可以立即算出新的最值;但如果要从 window 数组中减少一个数,就不能直接得到最值了,因为如果减少的这个数恰好是 A,就需要遍历 window 中的所有元素重新寻找新的最值。 可以使用 优先级队列 来动态寻找最值,通过创建一个大(小)顶堆,可以很快拿到最大(小)值。 但优先级队列无法满足标准队列结构【先进先出】的时间顺序,因为优先级队列底层利用二叉堆对元素进行动态排序,元素的出队顺序是元素的大小顺序,和入队的先后顺序完全没有关系。 而【单调队列】结构,既能够维护队列元素【先进先出】的时间顺序,又能够正确维护队列中所有元素的最值。 ","date":"2023-01-15","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/:0:0","tags":["labuladong","算法","队列和栈","滑动窗口"],"title":"【队列和栈】单调队列解决滑动窗口问题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"1 题目 力扣 239. 滑动窗口最大值 给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。 ","date":"2023-01-15","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/:1:0","tags":["labuladong","算法","队列和栈","滑动窗口"],"title":"【队列和栈】单调队列解决滑动窗口问题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2 解析 ","date":"2023-01-15","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/:2:0","tags":["labuladong","算法","队列和栈","滑动窗口"],"title":"【队列和栈】单调队列解决滑动窗口问题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.1 搭建解题框架 🟧 普通队列的标准 API: class Queue{ // enqueue 操作,在队尾加⼊元素 n void push(int n); // dequeue 操作,删除队头元素 void pop(); } 🟨 【单调队列】的 API: class MonotonicQueue{ // 在队尾添加元素 n void push(int n); // 返回当前队列中的最⼤值 int max(); // 队头元素如果是 n,删除它 void pop(int n); } 🟩 【滑动窗口】问题的解答框架 int[] maxSlidingWindow(int[] nums, int k) { MonotonicQueue window = new MonotonicQueue(); List\u003cInteger\u003e res = new ArrayList\u003c\u003e(); for (int i = 0; i \u003c nums.length; i++) { if (i \u003c k - 1) { //先把窗⼝的前 k - 1 填满 window.push(nums[i]); } else { // 窗⼝开始向前滑动 // 移⼊新元素 window.push(nums[i]); // 将当前窗⼝中的最⼤元素记⼊结果 res.add(window.max()); // 移出最后的元素 window.pop(nums[i - k + 1]); } } // 将 List 类型转化成 int[] 数组作为返回值 int[] arr = new int[res.size()]; for (int i = 0; i \u003c res.size(); i++) { arr[i] = res.get(i); } return arr; } ","date":"2023-01-15","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/:2:1","tags":["labuladong","算法","队列和栈","滑动窗口"],"title":"【队列和栈】单调队列解决滑动窗口问题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.2 实现单调队列数据结构 🟠 实现「单调队列」必须使用一种数据结构支持在头部和尾部进行插入和删 除,很明显双链表满足这个条件。 🟡 由于只输出每个窗口内的最大元素,所以实现 push 方法时依然在队尾添加元素,但要把前面比自己小的元素都删掉。 class MonotonicQueue { // 双链表,⽀持头部和尾部增删元素 // 维护其中的元素⾃尾部到头部单调递增 private LinkedList\u003cInteger\u003e maxq = new LinkedList\u003c\u003e(); // 在尾部添加⼀个元素 n,维护 maxq 的单调性质 public void push(int n) { // 将前⾯⼩于⾃⼰的元素都删除 while (!maxq.isEmpty() \u0026\u0026 maxq.getLast() \u003c n) { maxq.pollLast(); } maxq.addLast(n); } } 🟢 最终单调队列的元素会保持 单调递减 的顺序,因此 max 方法只用返回队首元素: public int max() { // 队头的元素肯定是最⼤的 return maxq.getFirst(); } 🔵 pop 方法在队头删除元素 n : public void pop(int n) { if (n == maxq.getFirst()) { maxq.pollFirst(); } } 完整代码: /* 单调队列的实现 */ class MonotonicQueue { LinkedList\u003cInteger\u003e maxq = new LinkedList\u003c\u003e(); public void push(int n) { // 将⼩于 n 的元素全部删除 while (!maxq.isEmpty() \u0026\u0026 maxq.getLast() \u003c n) { maxq.pollLast(); } // 然后将 n 加⼊尾部 maxq.addLast(n); } public int max() { return maxq.getFirst(); } public void pop(int n) { if (n == maxq.getFirst()) { maxq.pollFirst(); } } } /* 解题函数的实现 */ int[] maxSlidingWindow(int[] nums, int k) { MonotonicQueue window = new MonotonicQueue(); List\u003cInteger\u003e res = new ArrayList\u003c\u003e(); for (int i = 0; i \u003c nums.length; i++) { if (i \u003c k - 1) { //先填满窗⼝的前 k - 1 window.push(nums[i]); } else { // 窗⼝向前滑动,加⼊新数字 window.push(nums[i]); // 记录当前窗⼝的最⼤值 res.add(window.max()); // 移出旧数字 window.pop(nums[i - k + 1]); } } // 需要转成 int[] 数组再返回 int[] arr = new int[res.size()]; for (int i = 0; i \u003c res.size(); i++) { arr[i] = res.get(i); } return arr; } 🟣 nums 中的每个元素最多被 push 和 pop ⼀次,没有任何多余操作,所以整体的复杂度是 O(N)。空间复杂度就是窗口的大小 O(k)。 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-15","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/:2:2","tags":["labuladong","算法","队列和栈","滑动窗口"],"title":"【队列和栈】单调队列解决滑动窗口问题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"1 单调栈模板 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:1:0","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"题目 输入一个数组 nums,请你返回⼀个等长的结果数组,结果数组中对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:1:1","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"解析 🔴 把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列。如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的下⼀个更大元素,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。 🟡 for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈,其实是正着出栈。 🟢 while 循环是把两个「高个子」元素之间的元素排除,因为他们的存在没有意义,前面挡着个「更高」的元素,所以他们不可能被作为后续进来的元素的下一个更大元素了。 🔵 这个算法的复杂度只有 O(n)。 总共有 n 个元素,每个元素都被 push 入栈了一次,而最多会被 pop一次,没有任何冗余操作。 图解: 代码实现: int[] nextGreaterElement(int[] nums) { int n = nums.length; // 存放答案的数组 int[] res = new int[n]; Stack\u003cInteger\u003e s = new Stack\u003c\u003e(); // 倒着往栈⾥放 for (int i = n - 1; i \u003e= 0; i--) { // 判定个⼦⾼矮 while (!s.isEmpty() \u0026\u0026 s.peek() \u003c= nums[i]) { // 矮个起开,反正也被挡着了。。。 s.pop(); } // nums[i] 身后的更⼤元素 res[i] = s.isEmpty() ? -1 : s.peek(); s.push(nums[i]); } return res; } ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:1:2","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"2 进阶——下一个更大元素 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:2:0","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 496. 下一个更大元素 I nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。 给你两个 没有重复元素 的数组 nums1 和 nums2 ,下标从 0 开始计数,其中nums1 是 nums2 的子集。 对于每个 0 \u003c= i \u003c nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j] 的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1 。 返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素 。 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:2:1","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"解析 🟠 和上一题类似,只需要先把 nums2 中每个元素的下一个更大元素算出来存到一个映射里,然后再让 nums1 中的元素去查表即可。 🟣 使用 HashMap 映射,可以提高代码效率。 代码实现: public int[] nextGreaterElement(int[] nums1, int[] nums2) { // 记录 nums2 中每个元素的下⼀个更⼤元素 int[] greater=nextGreaterElement(nums2); // 转化成映射:元素 x -\u003e x 的下⼀个最⼤元素 HashMap\u003cInteger, Integer\u003e greaterMap=new HashMap\u003c\u003e(); for(int i=0; i\u003cnums2.length; i++){ greaterMap.put(nums2[i], greater[i]); } // nums1 是 nums2 的⼦集,所以根据 greaterMap 可以得到结果 int[] res=new int[nums1.length]; for(int i=0; i\u003cnums1.length; i++){ res[i]=greaterMap.get(nums1[i]); } return res; } ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:2:2","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"3 进阶——下一个更大元素索引 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:3:0","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 739. 每日温度 给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:3:1","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"解析 🟡 这个问题本质上也是找下一个更大元素,只不过现在不是问你下一个更大元素的值是多少,而是问你当前元素距离下一个更大元素的索引距离而已。 🟤 把之前记录最大值的堆栈改为记录最大值索引的堆栈即可。 或是使用两个堆栈,一个记录索引,一个记录最大值,不过此时代码效率会降低。 代码实现: public int[] dailyTemperatures(int[] temperatures) { int n=temperatures.length; int[] res=new int[n]; // 这⾥放元素索引,⽽不是元素 Stack\u003cInteger\u003e index=new Stack\u003c\u003e(); /* 单调栈模板 */ for(int i=n-1; i\u003e=0; i--){ while(!index.isEmpty() \u0026\u0026 temperatures[index.peek()]\u003c=temperatures[i]){ index.pop(); } // 得到索引间距 res[i] = index.isEmpty() ? 0 : index.peek()-i; // 将索引⼊栈,⽽不是元素 index.push(i); } return res; } ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:3:2","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"4 进阶——处理环形数组 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:4:0","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 503. 下一个更大元素 II 给定一个循环数组 nums ( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。 数字 x 的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1 。 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:4:1","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"解析 🟠 我们一般通过 % 运算符求模(余数),来模拟环形特效。 🟢 这道题的难点在于,比如输入是 [2,1,2,4,3],对于最后一个元素 3,如何找到元素 4 作为下一个更大元素。 🔵 对于这种需求,常用套路就是将数组长度翻倍。 🟣 我们可以不用构造翻倍的新数组,而是利用循环数组的技巧来模拟数组长度翻倍的效果。 图解: 代码实现: public int[] nextGreaterElements(int[] nums) { int n=nums.length; int[] res=new int[n]; Stack\u003cInteger\u003e s=new Stack\u003c\u003e(); // 数组⻓度加倍模拟环形数组 for(int i=2*n-1; i\u003e=0; i--){ // 索引 i 要求模,其他的和模板⼀样 while(!s.isEmpty() \u0026\u0026 s.peek()\u003c=nums[i%n]){ s.pop(); } res[i%n] = s.isEmpty() ? -1 : s.peek(); s.push(nums[i%n]); } return res; } 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:4:2","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"1.1 判断有效括号串 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:1:0","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 给定一个只包括 '(',')' 的字符串 s ,判断字符串是否有效。即每个右括号 ')' 的左边必须有⼀个左括号 '(' 和它匹配。 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:1:1","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 用一个变量 left 记录 '(' 相对于 ')' 的数量,遇到 '(' 就 +1,遇到 ')' 就 -1。如果最后 left==0,则括号串有效,否则无效。并且,如果中间出现 left 数量为负,则说明有 ')' 出现在 '(' 之前,也为无效。 代码实现: bool isValid(string str) { // 待匹配的左括号数量 int left = 0; for (int i = 0; i \u003c str.size(); i++) { if (s[i] == '(') { left++; } else { // 遇到右括号 left--; } // 右括号太多 if (left == -1) return false; } // 是否所有的左括号都被匹配了 return left == 0; } ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:1:2","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"1.2 判断有效括号串进阶 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:2:0","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 20. 有效的括号 给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 每个右括号都有一个对应的相同类型的左括号。 示例: 输入:s = \"()[]{}\" 输出:true ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:2:1","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 使用⼀个名为 left 的栈代替之前思路中的 left 变量,遇到左括号就入栈,遇到右括号就去栈 中寻找最近的左括号,看是否匹配。 代码实现: class Solution { public: bool isValid(string s) { stack\u003cchar\u003e left; for(char c:s){ // c是左括号 if(c=='(' || c=='{' || c=='[') left.push(c); else{ // c是右括号且栈中最近的左括号匹配 if(!left.empty() \u0026\u0026 leftOf(c)==left.top()) left.pop(); // 栈中最近的左括号不匹配 else return false; } } // 是否所有的左括号都被匹配了 return left.empty(); } char leftOf(char c){ if(c==')') return '('; else if(c=='}') return '{'; else return '['; } }; ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:2:2","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.1 平衡括号串 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:3:0","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 921. 使括号有效的最少添加 只有满足下面几点之一,括号字符串才是有效的: 它是一个空字符串,或者 它可以被写成 AB (A 与 B 连接), 其中 A 和 B 都是有效字符串,或者 它可以被写作 (A),其中 A 是有效字符串。 给定一个括号字符串 s ,在每一次操作中,你都可以在字符串的任何位置插入一个括号 例如,如果 s = \"()))\" ,你可以插入一个开始括号为 \"(()))\" 或结束括号为 \"())))\" 。 返回 为使结果字符串 s 有效而必须添加的最少括号数。 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:3:1","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 核心思路是以左括号为基准,通过维护对右括号的需求数 need,来计算最小的插入次数。 需要注意两个地方: 🟠 need == -1 need == -1 意味着右括号太多,需要及时判断插入左括号。注意后面多出的左括号不能抵消前面多出的右括号。 🟡 返回 res+need res 记录左括号的插入次数,need 记录右括号的需求。 代码实现: class Solution { public: int minAddToMakeValid(string s) { int res=0; // res记录插入次数 int need=0; // need记录右括号的需求量 for(char c:s){ if(c=='('){ need++; } else if(c==')'){ need--; if(need==-1){ // 需要插入一个左括号 need++; res++; } } } return res+need; } }; ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:3:2","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.2 平衡括号串进阶 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:4:0","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 1541. 平衡括号字符串的最少插入次数 给你一个括号字符串 s ,它只包含字符 '(' 和 ')' 。一个括号字符串被称为平衡的当它满足: 任何左括号 '(' 必须对应两个连续的右括号 '))' 。 左括号 '(' 必须在对应的连续两个右括号 '))' 之前。 比方说 \"())\", \"())(())))\" 和 \"(())())))\" 都是平衡的, \")()\", \"()))\" 和 \"(()))\" 都是不平衡的。 你可以在任意位置插入字符 ‘(’ 和 ‘)’ 使字符串平衡。 请你返回让 s 平衡的最少插入次数。 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:4:1","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 🟧 首先,按照上面的思路维护 res 和 need 变量 🟨 need == -1 当 need == -1 时,说明遇到一个多余的右括号,则需要插入一个左括号,对右括号的需求变为 1。即 res++; need = 1; 🟩 遇到左括号 对右括号的需求量 +2。若此时 need 为奇数,则需要插入一个右括号,使 need 变为偶数。即 res++; need--; 代码实现: public: int minInsertions(string s) { int res=0; // 记录插入的括号数量 int need=0; // 记录右括号的需求量 for(char c:s){ if(c=='('){ need+=2; // 一个左括号对应两个右括号 if(need%2 == 1){ res++; // 插入一个右括号 need--; // 对右括号的需求减一 } } else{ need--; if(need==-1){ // 右括号多了 need=1; // 还需要再来一个右括号 res++; // 插入一个左括号 } } } return res+need; } 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:4:2","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"1 递归反转整个链表 ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:1:0","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 206. 反转链表 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 示例: 输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1] ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:1:1","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"解析 图解: 代码实现: public ListNode reverseList(ListNode head) { if(head==null || head.next==null){ return head; } ListNode last=reverseList(head.next); head.next.next=head; head.next=null; return last; } 2个注意点: 🟡 递归函数要有 base case,如果链表为空或者只有⼀个节点的时候,反转结果就是它自己。 if (head == null || head.next == null) { return head; } 🟢 当链表递归反转之后,新的头结点是 last,而之前的 head 变成了最后⼀个节点,别忘了链表的末尾要指向 null。 head.next = null; ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:1:2","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"2 递归反转前 N 个节点 图解: 代码实现: ListNode successor = null; // 后驱节点 // 反转以 head 为起点的 n 个节点,返回新的头结点 ListNode reverseN(ListNode head, int n) { if (n == 1) { // 记录第 n + 1 个节点 successor = head.next; return head; } // 以 head.next 为起点,需要反转前 n - 1 个节点 ListNode last = reverseN(head.next, n - 1); head.next.next = head; // 让反转之后的 head 节点和后⾯的节点连起来 head.next = successor; return last; } 2 个区别: 🟠 base case 变为 n == 1,反转⼀个元素,就是它本身,同时要记录后驱节点。 🔵 刚才我们直接把 head.next 设置为 null,因为整个链表反转后原来的 head 变成了整个链表的最后⼀个节点。但现在 head 节点在递归反转之后不⼀定是最后⼀个节点了,所以要记录后驱 successor(第 n + 1 个节点),反转之后将 head 连接上。 ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:2:0","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"3 递归反转部分节点 ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:3:0","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 92. 反转链表 II 给你单链表的头指针 head 和两个整数 left 和 right ,其中 left \u003c= right 。 请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。 示例: 输入:head = [1,2,3,4,5], left = 2, right = 4 输出:[1,4,3,2,5] ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:3:1","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"解析 图解: 🟣 如果 m == 1,就相当于反转链表开头的 n 个元素,也就是刚刚实现的 reverseN () 。 🟤 如果 m != 1 ,我们把 head 的索引视为 1,即从第 m 个元素开始反转;如果把 head.next 的索引视为 1 ,那么反转的区间应该从第 m - 1 个元素开始;以此类推…… 代码实现: ListNode successor = null; public ListNode reverseBetween(ListNode head, int left, int right) { // base case if(left==1) return reverseN(head,right-left+1); // 前进到反转的起点触发 base case head.next=reverseBetween(head.next,left-1,right-1); return head; } 💥💥💥 递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)。 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:3:2","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 870. 优势洗牌 给定两个大小相等的数组 nums1 和 nums2,nums1 相对于 nums2 的优势可以用满足 nums1[i] \u003e nums2[i] 的索引 i 的数目来描述。 返回 nums1 的任意排列,使其相对于 nums2 的优势最大化。 ","date":"2023-01-08","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E7%94%B0%E5%BF%8C%E8%B5%9B%E9%A9%AC%E8%83%8C%E5%90%8E%E7%9A%84%E7%AE%97%E6%B3%95%E5%86%B3%E7%AD%96/:1:0","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】田忌赛马背后的算法决策","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E7%94%B0%E5%BF%8C%E8%B5%9B%E9%A9%AC%E8%83%8C%E5%90%8E%E7%9A%84%E7%AE%97%E6%B3%95%E5%86%B3%E7%AD%96/"},{"categories":["labuladong的算法秘籍"],"content":"解析 这道题类似于【田忌赛马】,只不过马的数量变多了,精髓在于【打得过就打,打不过就拿自己的垃圾和对方的精锐互换 】。我们先分析【田忌赛马】,考虑以下 3 点: 🟡 如果田忌的 1 号选手 \u003c 齐王的 1 号选手,显然应该用田忌垫底的马送人头,降低对方的战斗力。 🟢 如果田忌的 1 号选手 \u003c 齐王的 1 号选手,则应该直接让两者相比。 🟠 当出现第二种情况时,即 T1 \u003e Q1 时,要不要节约战斗力,用 T2 对抗 Q1? 答案是不需要。假设 T2 \u003e Q1,那么不论换不换 T1,T1 和 T2 都能对抗所有的 Q,这种节约毫无意义。 根据以上思路,我们的策略是: 将齐王和田忌的马按照战斗力排序,然后按照排名一一对比。如果田忌的马能赢,那就比赛,如果赢不了,那就换个垫底的来送人头,保存实力。 结合已学过的双指针技巧,代码实现如下: class Solution { public int[] advantageCount(int[] nums1, int[] nums2) { int n=nums1.length; // 给 nums2 降序排序 PriorityQueue\u003cint[]\u003e maxq = new PriorityQueue\u003c\u003e( (int[] pair1, int[] pair2) -\u003e { return pair2[1] - pair1[1]; } ); for(int i=0; i\u003cn; i++) maxq.offer(new int[] {i, nums2[i]}); // 给 numsq 升序排序 Arrays.sort(nums1); int left=0, right=n-1; int[] res = new int[n]; while(!maxq.isEmpty()){ int[] pair=maxq.poll(); // val 是nums2 中的最大值,i 是对应索引 int i=pair[0], val=pair[1]; if(val\u003cnums1[right]){ // 最大值能比过就直接比 res[i]=nums1[right--]; } else{ // 否则用最小值和最大值比 res[i]=nums1[left++]; } } return res; } } 算法的时间复杂度,也就是二叉堆和排序的复杂度 O(nlogn)。 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-08","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E7%94%B0%E5%BF%8C%E8%B5%9B%E9%A9%AC%E8%83%8C%E5%90%8E%E7%9A%84%E7%AE%97%E6%B3%95%E5%86%B3%E7%AD%96/:2:0","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】田忌赛马背后的算法决策","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E7%94%B0%E5%BF%8C%E8%B5%9B%E9%A9%AC%E8%83%8C%E5%90%8E%E7%9A%84%E7%AE%97%E6%B3%95%E5%86%B3%E7%AD%96/"},{"categories":["labuladong的算法秘籍"],"content":"二分思维的精髓就是:通过已知信息尽可能多地收缩搜索空间,从而增加穷举效率,快速找到目标。 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:0:0","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"1 二分查找框架 int binarySearch(int[] nums, int target) { int left = 0, right = ...; while(...) { int mid = left + (right - left) / 2; if (nums[mid] == target) { ... } else if (nums[mid] \u003c target) { left = ... } else if (nums[mid] \u003e target) { right = ... } } return ...; } 计算 mid 时需要防止溢出,代码中 left + (right - left) / 2 就和 (left + right) / 2 的结果相同,但是有效防止了 left 和 right 太大,直接相加导致溢出的情况。 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:1:0","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"2 寻找一个数 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:2:0","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 704. 二分查找 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:2:1","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"解析 int binarySearch(int[] nums, int target) { int left = 0; int right = nums.length - 1; // 注意 while(left \u003c= right) { int mid = left + (right - left) / 2; if(nums[mid] == target) return mid; else if (nums[mid] \u003c target) left = mid + 1; // 注意 else if (nums[mid] \u003e target) right = mid - 1; // 注意 } return -1; } 🟠 while 循环的条件中是 \u003c=,而不是 \u003c 因为初始化 right 的赋值是 nums.length - 1,而不是 nums.length。前者相当于闭区间 [left, right],后者相当于左闭右开区间 [left, right),因为索引大小为 nums.length 是越界的。 而只有搜索区间为空时才停止寻找,即 left \u003c= right 时都应该继续循环。 🟡 算法缺陷 不能给出左侧 / 右侧边界。例如有序数组 nums = [1,2,2,2,3],target 为 2,左侧索引为 1,右侧索引为 3 。按照该算法只能求出索引 2。 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:2:2","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"3 寻找左侧边界的二分搜索 int left_bound(int[] nums, int target) { int left = 0, right = nums.length - 1; // 搜索区间为 [left, right] while (left \u003c= right) { int mid = left + (right - left) / 2; if (nums[mid] \u003c target) { // 搜索区间变为 [mid+1, right] left = mid + 1; } else if (nums[mid] \u003e target) { // 搜索区间变为 [left, mid-1] right = mid - 1; } else if (nums[mid] == target) { // 收缩右侧边界 right = mid - 1; } } // 判断 target 是否存在于 nums 中 // 此时 target ⽐所有数都⼤,返回 -1 if (left == nums.length) return -1; // 判断⼀下 nums[left] 是不是 target return nums[left] == target ? left : -1; } ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:3:0","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"4 寻找右侧边界的二分查找 int right_bound(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left \u003c= right) { int mid = left + (right - left) / 2; if (nums[mid] \u003c target) { left = mid + 1; } else if (nums[mid] \u003e target) { right = mid - 1; } else if (nums[mid] == target) { // 这⾥改成收缩左侧边界即可 left = mid + 1; } } // 最后改成返回 left - 1 if (left - 1 \u003c 0) return -1; return nums[left - 1] == target ? (left - 1) : -1; } ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:4:0","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"5 综合 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:5:0","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 34. 在排序数组中查找元素的第一个和最后一个位置 给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。 如果数组中不存在目标值 target,返回 [-1, -1]。 你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:5:1","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"解析 综合使用上面的左侧二分查找、右侧二分查找即可。代码实现如下:(左侧二分查找和右侧二分查找代码省略,上面已有) class Solution { public int[] searchRange(int[] nums, int target) { int index1=left_bound(nums,target); int index2=right_bound(nums,target); return new int[] {index1,index2}; } } 以上就是二分查找算法的细节。在写二分查找代码时,注意以下 4 点: 🟢 分析二分查找代码时,不要出现 else,全部展开成 else if 方便理解。 🔵 注意「搜索区间」和 while 的终止条件,如果存在漏掉的元素,记得在最后检查。 🟣 如需定义左闭右开的「搜索区间」搜索左右边界,只要在 nums[mid] == target 时做修改即可,搜索右侧时需要减一。 🟤 如果将「搜索区间」全都统一成两端都闭,好记,只要稍改 nums[mid] == target 条件处的代码和返回的逻辑即可。 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:5:2","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"本文主要记录最难掌握的一类双指针技巧——滑动窗口算法。算法思路很简单,就是维护一个窗口,不断滑动,时间复杂度为 O(N)。大致逻辑如下: int left = 0, right = 0; while (right \u003c s.size()) { // 增⼤窗⼝ window.add(s[right]); right++; while (window needs shrink) { // 缩⼩窗⼝ window.remove(s[left]); left++; } } 滑动窗口算法的代码框架如下: /* 滑动窗⼝算法框架 */ void slidingWindow(string s) { unordered_map\u003cchar, int\u003e window; int left = 0, right = 0; while (right \u003c s.size()) { // c 是将移⼊窗⼝的字符 char c = s[right]; // 增⼤窗⼝ right++; // 进⾏窗⼝内数据的⼀系列更新 ... /*** debug 输出的位置 ***/ printf(\"window: [%d, %d)\\n\", left, right); /********************/ // 判断左侧窗⼝是否要收缩 while (window needs shrink) { // d 是将移出窗⼝的字符 char d = s[left]; // 缩⼩窗⼝ left++; // 进⾏窗⼝内数据的⼀系列更新 ... } } } unordered_map 就是哈希表(字典),相当于 Java 的 HashMap,它的⼀个方法 count(key) 相当于 Java 的 containsKey(key) 可以判断键 key 是否存在。可以使用方括号访问键对应的值 map[key]。需要注意的是,如果该 key 不存在,C++ 会自动创建这个 key,并把 map[key] 赋值为 0。 套模板前需要思考 3 个问题: ✅ 什么时候移动 right 扩大窗口? ✅ 什么时候移动 left 缩小窗口? ✅ 什么时候更新结果? ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:0:0","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"1 最小覆盖子串 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:1:0","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 76. 最小覆盖子串 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 \"\" 。 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:1:1","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"解析 🟡 我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。 理论上区间可以两端都开或者两端都闭,但左闭右开最放便处理。因为初始化时区间 [0, 0) 没有元素,但 right 向右移动一位,区间 [0, 1) 就包含一个元素 0 了。如果区间的两端都开,那么 right 向右移动一位后区间 (0, 1) 仍没有元素;如果区间两端都闭,那么 [0, 0] 未初始化就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。 🟢 不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求 🔵 停止增加 right,转而不断增加 left 指针缩小窗口[left, right),直到窗口中的字符串不再符合要求。同时,每次增加 left,都要更新一轮结果。 🟣 重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。 如果⼀个字符进入窗口,应该增加 window 计数器;如果⼀个字符将移出窗口的时候,应该减少 window 计数器;当 count 满足 need 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。 class Solution { public: string minWindow(string s, string t) { unordered_map\u003cchar, int\u003e need, window; for(char c:t) need[c]++; int left=0,right=0; // 记录最小覆盖子串的起始索引及长度 int start=0,len=INT_MAX; // 记录窗口内含有规定字符的个数(无重复) int count=0; while(right\u003cs.size()){ char c=s[right]; // 移入窗口的字符 right++; // 扩大窗口 if(need.count(c)){ window[c]++; if(window[c]==need[c]) count++; } // 判断窗口是否要缩小 while(count==need.size()){ // 更新最小覆盖子串 if(right-left \u003c len){ start=left; len = right-left; } char a=s[left]; // 移出窗口的字符 left++; // 缩小窗口 // 更新数据 if(need.count(a)){ if(window[a]==need[a]) count--; window[a]--; } } } return len==INT_MAX ? \"\" : s.substr(start,len); } }; ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:1:2","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"2 字符串的排列 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:2:0","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 567. 字符串的排列 给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。 换句话说,s1 的排列之一是 s2 的 子串 。 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:2:1","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"解析 本题移动 left 缩小窗口 的时机是窗口大小大于 t.size() 时,因为排列嘛,显然长度应该是⼀样的。 当发现 valid == need.size() 时,就说明窗口中就是⼀个合法的排列,所以立即返回 true。 至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同 class Solution { public: bool checkInclusion(string s1, string s2) { unordered_map\u003cchar, int\u003e need, window; for (char c : s1) need[c]++; int left=0,right=0; int count=0; while(right\u003cs2.size()){ char a=s2[right]; right++; // 更新窗口数据 if(need.count(a)){ window[a]++; if(window[a]==need[a]) count++; } // 判断左侧窗口是否收缩 while(right-left \u003e= s1.size()){ // 在这⾥判断是否找到了合法的⼦串 if(count==need.size()) return true; char b=s2[left]; left++; // 更新窗口数据 if(need.count(b)){ if(window[b]==need[b]) count--; window[b]--; } } } return false; } }; ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:2:2","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"3 找所有字母异位词 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:3:0","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 438. 找到字符串中所有字母异位词 给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。 异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:3:1","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"解析 class Solution { public: vector\u003cint\u003e findAnagrams(string s, string p) { unordered_map\u003cchar, int\u003e need, window; for (char c : p) need[c]++; int left=0,right=0; int count=0; vector\u003cint\u003e res; // 记录结果 while(right\u003cs.size()){ char a=s[right]; right++; // 更新窗口数据 if(need.count(a)){ window[a]++; if(window[a]==need[a]) count++; } // 判断左侧窗口是否要收缩 while(right-left\u003e=p.size()){ if(count==need.size()) res.push_back(left); char b=s[left]; left++; // 更新窗口数据 if(need.count(b)){ if(window[b]==need[b]) count--; window[b]--; } } } return res; } }; ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:3:2","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"4 最长无重复子串 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:4:0","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 3. 无重复字符的最长子串 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:4:1","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"解析 class Solution { public: int lengthOfLongestSubstring(string s) { unordered_map\u003cchar, int\u003e window; int left=0,right=0; int res=0; // 记录结果 while(right\u003cs.size()){ char a=s[right]; right++; // 更新数据 window[a]++; // 判断左侧窗口是否要收缩 while(window[a]\u003e1){ char b=s[left]; left++; window[b]--; // 更新数据 } // 更新答案 res = right-left\u003eres ? right-left : res; } return res; } }; 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:4:2","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"在处理数组和链表相关问题时,双指针技巧是经常用到的,双指针技巧主要分为两类:左右指针和快慢指针。所谓左右指针,就是两个指针相向而行或者相背而行;而所谓快慢指针,就是两个指针同向而行。 ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:0:0","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"1 快慢指针技巧 题目 力扣 26. 删除有序数组中的重复项 给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。 将最终结果插入 nums 的前 k 个位置后返回 k 。 不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 解析 我们让慢指针 slow 走在后面,快指针 fast 走在前面探路,找到⼀个不重复的元素就赋值给 slow 并让 slow 前进⼀步。 这样,就保证了 nums[0..slow] 都是无重复的元素,当 fast 指针遍历完整个数组 nums 后,nums[0..slow] 就是整个数组去重之后的结果。 代码实现: class Solution { public int removeDuplicates(int[] nums) { if(nums.length == 0) return 0; int slow=0,fast=0; while(fast\u003cnums.length){ if(nums[slow] != nums[fast]){ slow++; nums[slow]=nums[fast]; } fast++; } return slow+1; } } 进阶1 力扣 83. 删除排序链表中的重复元素 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回已排序的链表 。 代码实现: class Solution { public ListNode deleteDuplicates(ListNode head) { if(head == null) return head; ListNode slow=head,fast=head; while(fast != null){ if(slow.val != fast.val){ slow.next = fast; slow = slow.next; } fast = fast.next; } slow.next = null; return head; } } 进阶2 力扣 27. 移除元素 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。 不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。 解析: 和前面类似,依然需要使用快慢指针技巧。如果 fast 遇到值为 val 的元素,则直接跳过,否则就赋值给 slow 指针,并让 slow 前进⼀步。 代码实现: class Solution { public int removeElement(int[] nums, int val) { int fast = 0, slow = 0; while (fast \u003c nums.length) { if (nums[fast] != val) { nums[slow] = nums[fast]; slow++; } fast++; } return slow; } } 进阶3 力扣 283. 移动零 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 和上一题思路类似,代码实现: class Solution { public void moveZeroes(int[] nums) { int fast = 0, slow = 0; while (fast \u003c nums.length) { if (nums[fast] != 0) { nums[slow] = nums[fast]; slow++; } fast++; } for(int i=slow; i\u003cnums.length; i++) nums[i] = 0; } } ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:1:0","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2 左右指针技巧 ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:2:0","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.1 二分查找 int binarySearch(int[] nums, int target) { // ⼀左⼀右两个指针相向⽽⾏ int left = 0, right = nums.length - 1; while(left \u003c= right) { int mid = (right + left) / 2; if(nums[mid] == target) return mid; else if (nums[mid] \u003c target) left = mid + 1; else if (nums[mid] \u003e target) right = mid - 1; } return -1; } ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:2:1","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.2 两数之和 题目 力扣 167. 两数之和 II - 输入有序数组 给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 \u003c= index1 \u003c index2 \u003c= numbers.length 。以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。 你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。你所设计的解决方案必须只使用常量级的额外空间。 解析 使用双指针,通过调节 left 和 right 就可以调整 sum 的大小。代码实现: class Solution { public int[] twoSum(int[] numbers, int target) { int left=0, right=numbers.length-1; while(left\u003cright){ if(numbers[left]+numbers[right]==target) // 题目要求的索引是从1开始的 return new int[]{left+1,right+1}; else if(numbers[left]+numbers[right]\u003ctarget) left++; // 让和大一点 else right--; // 让和小一点 } return new int[]{-1,-1}; } } ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:2:2","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.3 反转数组 题目 力扣 344. 反转字符串 编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。 不要给另外的数组分配额外的空间,你必须 原地 修改输入数组、使用 O(1) 的额外空间解决这一问题。 解析 通过左右指针反转,思路很简单。 注意:temp 的声明在 while 循环里面内存占用会比下面的多很多。在以后的编程中要注意。 class Solution { public void reverseString(char[] s) { int left=0, right=s.length-1; char temp; while(left\u003cright){ // 交换s[left]和s[right] temp=s[left]; s[left]=s[right]; s[right]=temp; left++; right--; } } } ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:2:3","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.4 回文串判断 解析 使用左右指针判断回文串,代码如下: boolean isPalindrome(String s) { // ⼀左⼀右两个指针相向⽽⾏ int left = 0, right = s.length() - 1; while (left \u003c right) { if (s.charAt(left) != s.charAt(right)) { return false; } left++; right--; } return true; } 进阶 力扣 5. 最长回文子串 给你一个字符串 s,找到 s 中最长的回文子串。 解析 使用从中心向两端扩散的双指针技巧。如果回文串的长度为奇数,则它有⼀个中心字符;如果回文串的长度为偶数,则可以认为它有两个中心字符。 我们遍历整个数组,并分别求出以 s[i] 和 s[i,j] 为中心的最长回文串,保留两者中更长的,最终可以得到所求结果。 class Solution { public String longestPalindrome(String s) { String res1,res2,res=\"\"; for(int i=0; i\u003cs.length(); i++){ // 以s[i]为中心的最长回文子串 res1 = palindrome(s,i,i); // 以s[i]和s[i+1]为中心的最长回文子串 res2 = palindrome(s,i,i+1); res = res.length()\u003eres1.length() ? res : res1; res = res.length()\u003eres2.length() ? res : res2; } return res; } // 在s中寻找以s[l]和s[r]为中心的最长回文串 public String palindrome(String s, int l, int r){ while(l\u003e=0 \u0026\u0026 r\u003cs.length() \u0026\u0026 s.charAt(l)==s.charAt(r)){ l--; r++; } return s.substring(l+1,r); } } 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:2:4","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"1 合并两个有序链表 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:1:0","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 21. 合并两个有序链表 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例: 输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4] ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:1:1","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 这题比较简单,直接用 while 循环每次比较 p1 和 p2 的大小,把较小的节点接到结果链表上,如图所示: 虚拟头节点技巧 通过虚拟头节点这个占位符,可以避免处理空指针的情况,降低代码的复杂性。 当你需要创造⼀条新链表的时候,可以使⽤虚拟头结点简化边界情况的处理。 最后通过 p.next = n1; 将剩余的节点全都复制过去,不需要使用 while 循环一条一条复制。 代码实现: class Solution { public ListNode mergeTwoLists(ListNode list1, ListNode list2) { // 虚拟头结点 ListNode p = new ListNode(), res = p; ListNode n1 = list1, n2 = list2; while(n1!=null \u0026\u0026 n2!=null){ // ⽐较 n1 和 n2 两个指针 // 将值较⼩的的节点接到 p 指针 if(n1.val \u003c n2.val){ p.next = n1; n1 = n1.next; } else { p.next = n2; n2 = n2.next; } // p 指针不断前进 p = p.next; } // 最后还剩的直接一次复制过去 if(n1 != null){ p.next = n1; } if(n2!=null){ p.next = n2; } return res.next; } } ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:1:2","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2 单链表的分解 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:2:0","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 86. 分隔链表 给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你应当 保留 两个分区中每个节点的初始相对位置。 示例: 输入:head = [1,4,3,2,5,2], x = 3 输出:[1,2,2,4,3,5] ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:2:1","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 把原链表分成两个小链表,⼀个链表中的元素大小都小于 x,另⼀个链表中的元素都大于等于 x,最后再把这两条链表接到⼀起,就得到了题目想要的结果。 ❗❗❗注意:最后合并时,要先断开 bp 的 next 指针。因为 bp 是直接由 hp 复制过来的,后面可能还接着小于 x 的节点,因此可能在答案链表中形成循环。 class Solution { public ListNode partition(ListNode head, int x) { // 存放大于 x 的链表的虚拟头节点 ListNode big = new ListNode(), bp = big; // 存放小于 x 的链表的虚拟头节点 ListNode res = new ListNode(), rp = res; ListNode hp = head; // 用于遍历head // 将一个链表分解成2个 while(hp != null){ if(hp.val \u003c x){ rp.next = hp; // 利用虚拟节点 rp = rp.next; } else{ bp.next = hp; // 利用虚拟节点 bp = bp.next; } hp = hp.next; } bp.next = null; // 断开bp的next指针 rp.next = big.next; // 合并2个链表 return res.next; } } ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:2:2","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"3 合并 k 个有序链表 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:3:0","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 23. 合并K个升序链表 给你一个链表数组,每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中,返回合并后的链表。 示例 : 输入:lists = [[1,4,5],[1,3,4],[2,6]] 输出:[1,1,2,3,4,4,5,6] 解释:链表数组如下: [ 1-\u003e4-\u003e5, 1-\u003e3-\u003e4, 2-\u003e6 ] 将它们合并到一个有序链表中得到。 1-\u003e1-\u003e2-\u003e3-\u003e4-\u003e4-\u003e5-\u003e6 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:3:1","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 合并 k 个有序链表的逻辑类似合并两个有序链表,难点在于,如何快速得到 k 个节点中的最小节点,接到结果链表上? 这里我们就要用到 优先级队列(二叉堆) 这种数据结构,把链表节点放入⼀个最小堆,就可以每次获得 k 个节点中的最小节点: ListNode mergeKLists(ListNode[] lists) { if (lists.length == 0) return null; // 虚拟头结点 ListNode dummy = new ListNode(-1); ListNode p = dummy; // 优先级队列,最⼩堆 PriorityQueue\u003cListNode\u003e pq = new PriorityQueue\u003c\u003e( lists.length, (a, b)-\u003e(a.val - b.val)); // 将 k 个链表的头结点加⼊最⼩堆 for (ListNode head : lists) { if (head != null) pq.add(head); } while (!pq.isEmpty()) { // 获取最⼩节点,接到结果链表中 ListNode node = pq.poll(); p.next = node; if (node.next != null) { pq.add(node.next); } // p 指针不断前进 p = p.next; } return dummy.next; } 优先队列 pq 中的元素个数最多是 k,所以⼀次 poll 或者 add 方法的时间复杂度是 O(logk);所有的链表节点都会被加入和弹出 pq,所以算法整体的时间复杂度是 O(Nlogk),其中 k 是链表的条数,N 是这些链表的节点总数。 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:3:2","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"4 单链表的倒数第 k 个节点 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:0","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目—查找链表倒数第N个节点 寻找从后往前数的第 k 个节点。 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:1","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析1 一般做法:遍历 2 次链表 假设链表有 n 个节点,倒数第 k 个节点就是正数第 n - k + 1 个节点。由于算法题⼀般只给⼀个 ListNode 头结点代表⼀条单链表,所以不能直接得出这条链表的长度 n,需要先遍历⼀遍链表算出 n 的值,然后再遍历链表计算第 n - k + 1 个节点。也就是说,这个解法需要遍历两次链表才能得到出倒数第 k 个节点。 改进做法:遍历 1 次链表 首先,我们先让⼀个指针 p1 指向链表的头节点 head,然后走 k 步;再用⼀个指针 p2 指向链表头节点 head ,则 p1 和 p2 之间相差 k 步。当 p1 走到末尾空指针时,p2 刚好在倒数第 k 个的位置。如图所示: 代码实现如下: // 返回链表的倒数第 k 个节点 ListNode findFromEnd(ListNode head, int k) { ListNode p1 = head; // p1 先⾛ k 步 for (int i = 0; i \u003c k; i++) { p1 = p1.next; } ListNode p2 = head; // p1 和 p2 同时⾛ n - k 步 while (p1 != null) { p2 = p2.next; p1 = p1.next; } // p2 现在指向第 n - k + 1 个节点,即倒数第 k 个节点 return p2; } ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:2","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目—删除链表的倒数第N个结点 力扣 19. 删除链表的倒数第 N 个结点 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 示例: 输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:3","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 和上面思路类似,获得倒数第 n + 1 个节点的引用并进行删除。注意使用虚节点技巧,防止出现空指针的情况。代码如下: class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode p1 = new ListNode(); // 虚拟头结点 p1.next = head; // 删除倒数第 n 个,要先找到倒数第 n+1 个节点 ListNode p2 = findFromEnd(p1, n+1); p2.next = p2.next.next; return p1.next; } public ListNode findFromEnd(ListNode head, int n){ ListNode p1 = head; ListNode p2 = head; for(int i=0; i\u003cn; i++){ p1 = p1.next; } while(p1 != null){ p1 = p1.next; p2 = p2.next; } return p2; } } 5 单链表的中点 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:4","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 876. 链表的中间结点 给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。 示例 1: 输入:[1,2,3,4,5] 输出:此列表中的结点 3 (序列化形式:[3,4,5]) 返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。 注意,我们返回了一个 ListNode 类型的对象 ans,这样: ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL. ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:5","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 一般做法:遍历 2 次链表 需要遍历一次链表计算长度 n ,再遍历⼀次得到第 n / 2 个节点,也就是中间节点。 改进做法:遍历 1 次链表 使用【快慢指针】的技巧。我们让两个指针 slow 和 fast 分别指向链表头结点 head。每当慢指针 slow 前进⼀步,快指针 fast 就前进两步,这样,当 fast ⾛到链表末尾时,slow 就指向了链表中点。 代码实现如下: class Solution { ListNode middleNode(ListNode head) { // 快慢指针初始化指向 head ListNode slow = head, fast = head; // 快指针⾛到末尾时停⽌ while (fast != null \u0026\u0026 fast.next != null) { // 慢指针⾛⼀步,快指针⾛两步 slow = slow.next; fast = fast.next.next; } // 慢指针指向中点 return slow; } } 6 判断链表是否包含环 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:6","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 使用【快慢指针】。如果 fast 最终遇到空指针,说明链表中没有环;如果 fast 最终和 slow 相遇,那肯定是 fast 超过了 slow ⼀圈,说明链表中含有环。原理如图所示: boolean hasCycle(ListNode head) { // 快慢指针初始化指向 head ListNode slow = head, fast = head; // 快指针⾛到末尾时停⽌ while (fast != null \u0026\u0026 fast.next != null) { // 慢指针⾛⼀步,快指针⾛两步 slow = slow.next; fast = fast.next.next; // 快慢指针相遇,说明含有环 if (slow == fast) { return true; } } // 不包含环 return false; } ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:7","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"进阶 如果链表中含有环,如何计算这个环的起点? 当快慢指针相遇时,让其中任⼀个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所 在的节点位置就是环开始的位置。 代码如下: ListNode detectCycle(ListNode head) { ListNode fast, slow; fast = slow = head; while (fast != null \u0026\u0026 fast.next != null) { fast = fast.next.next; slow = slow.next; if (fast == slow) break; } // 上⾯的代码类似 hasCycle 函数 if (fast == null || fast.next == null) { // fast 遇到空指针说明没有环 return null; } // 重新指向头结点 slow = head; // 快慢指针同步前进,相交点就是环起点 while (slow != fast) { fast = fast.next; slow = slow.next; } return slow; } 7 两个链表是否相交 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:8","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 160. 相交链表 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。题目数据 保证 整个链式结构中不存在环。注意,函数返回结果后,链表必须 保持其原始结构 。图示两个链表在节点 c1 开始相交: ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:9","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 一般做法: 用 HashSet 记录⼀个链表的所有节点,然后和另⼀条链表对比,但这就需要额外的空间。 改进做法: 如果用两个指针 p1 和 p2 分别在两条链表上前进,并不能同时走到公共节点,也就无法得到相交节点 c1。 解决这个问题的关键是,通过某些方式,让 p1 和 p2 能够同时到达相交节点 c1。 所以,我们可以让 p1 遍历完链表 A 之后开始遍历链表 B,让 p2 遍历完链表 B 之后开始遍历链表 A,这样相当于【逻辑上】两条链表接在了⼀起。 p1 和 p2 就可以同时进入公共部分。图解如下: 代码实现如下: ListNode getIntersectionNode(ListNode headA, ListNode headB) { // p1 指向 A 链表头结点,p2 指向 B 链表头结点 ListNode p1 = headA, p2 = headB; while (p1 != p2) { // p1 ⾛⼀步,如果⾛到 A 链表末尾,转到 B 链表 if (p1 == null) p1 = headB; else p1 = p1.next; // p2 ⾛⼀步,如果⾛到 B 链表末尾,转到 A 链表 if (p2 == null) p2 = headA; else p2 = p2.next; } return p1; } 参考资料: https://labuladong.github.io/algo/ ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:10","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。 ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:0:0","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"1 原理 ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:0","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"题目 给出⼀个数组 nums,要求给区间 nums[2..6] 全部加 1,再给 nums[3..9] 全部减3,再给 nums[0..4] 全部加 2……N步操作后问,最后 nums 数组的值是什么? ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:1","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"解析 常规思路: 用for循环都给 nums[i…j] 加上 val ,时间复杂度为 O(N)。由于对 nums 频繁修改,效率很低。 差分数组: 对 nums 数组构造⼀个 diff 差分数组,diff[i] 就是 nums[i] 和 nums[i-1] 之差。原理如图: 这样构造差分数组 diff,就可以快速进行区间增减的操作,如果你想对区间 nums[i..j] 的元素全部加 3,那么只需要让 diff[i] += 3,然后再让 diff[j+1] -= 3 即可。 只要花费 O(1) 的时间修改 diff 数组,就相当于给 nums 的整个区间做了修改。多次修改 diff,然后通过 diff 数组反推,即可得到 nums 修改后的结果。 代码实现如下: // 差分数组⼯具类 class Difference { // 差分数组 private int[] diff; /* 输⼊⼀个初始数组,区间操作将在这个数组上进⾏ */ public Difference(int[] nums) { assert nums.length \u003e 0; diff = new int[nums.length]; // 根据初始数组构造差分数组 diff[0] = nums[0]; for (int i = 1; i \u003c nums.length; i++) { diff[i] = nums[i] - nums[i - 1]; } } /* 给闭区间 [i, j] 增加 val(可以是负数)*/ public void increment(int i, int j, int val) { diff[i] += val; if (j + 1 \u003c diff.length) { diff[j + 1] -= val; } } /* 返回结果数组 */ public int[] result() { int[] res = new int[diff.length]; // 根据差分数组构造结果数组 res[0] = diff[0]; for (int i = 1; i \u003c diff.length; i++) { res[i] = res[i - 1] + diff[i]; } return res; } } 注意 increment ⽅法中的 if 语句:当 j+1 \u003e= diff.length 时,说明是对 nums[i] 及以后的整个数组都进⾏修改,那么就不需要再给 diff 数组减 val 了。 ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:2","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"2 延伸——区间加法 ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:2:0","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 307. 区间加法 假设你有一个长度为 n 的数组,初始情况下所有数字均为 0 ,你将会被给出 k 个更新的操作。 其中,每个操作会被表示为一个三元组:[startIndex, endIndex, inc],你需要将子数组 A[startIndex ... endIndex](包括 startIndex 和 endIndex )增加 inc。 请返回 k 次操作后的数组。 示例: 输入: length = 5, update = [[1,3,2],[2,4,3],[0,2,-2]] 输出: [-2,0,3,5,3] 解释: 初始状态:[0,0,0,0,0] 进行了操作[1,3,2]后的状态:[0,2,2,2,0] 进行了操作[2,4,3]后的状态:[0,2,5,5,3] 进行了操作[0,2,-2]后的状态:[-2,0,3,5,3] ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:2:1","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"解析 使用刚才的 Difference 类: int[] getModifiedArray(int length, int[][] updates) { // nums 初始化为全 0 int[] nums = new int[length]; // 构造差分解法 Difference df = new Difference(nums); for (int[] update : updates) { int i = update[0]; int j = update[1]; int val = update[2]; df.increment(i, j, val); } return df.result(); } ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:2:2","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"3 延伸——航班预订系统 ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:3:0","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 1109. 航班预订统计 这里有 n 个航班,它们分别从 1 到 n 进行编号。 有一份航班预订表 bookings ,表中第 i 条预订记录 bookings[i] = [firsti, lasti, seatsi] 意味着在从 firsti 到 lasti (包含 firsti 和 lasti )的 每个航班 上预订了 seatsi 个座位。 请你返回一个长度为 n 的数组 answer,里面的元素是每个航班预定的座位总数。 示例 : 输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5 输出:[10,55,45,25,25] 解释: 航班编号 1 2 3 4 5 预订记录 1 : 10 10 预订记录 2 : 20 20 预订记录 3 : 25 25 25 25 总座位数: 10 55 45 25 25 因此,answer = [10,55,45,25,25] ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:3:1","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"解析 题目相当于:输入一个长度为 n 的数组 nums,其中所有元素都是 0。再给你输⼊⼀个 bookings,里面是若干三元组 (i, j, k),每个三元组的含义就是要求你给 nums 数组的闭区间 [i-1,j-1] 中所有元素都加上 k。请你返回最后的 nums 数组。 PS:因为题⽬说的 n 是从 1 开始计数的,⽽数组索引从 0 开始,所以对于输⼊的三元组 (i, j, k),数组区间应该对应 [i-1,j-1]。 这是一道标准的差分数组,利用上面的思想很容易实现: class Solution { private int[] diff; public int[] corpFlightBookings(int[][] bookings, int n) { // 构造差分数组 diff = new int[n]; for(int i=0; i\u003cbookings.length; i++){ increment(bookings[i][0],bookings[i][1],bookings[i][2],n); } return result(n); } public void increment(int first, int last, int seat, int n){ diff[first-1] += seat; if(last-1 \u003c n-1){ diff[last] -= seat; } } public int[] result(int n){ int[] res = new int[n]; res[0] = diff[0]; for(int i=1; i\u003cn; i++){ res[i] = diff[i]+res[i-1]; } return res; } } ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:3:2","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"4 延伸——拼车 ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:4:0","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 1094. 拼车 车上最初有 capacity 个空座位。车 只能 向一个方向行驶(也就是说,不允许掉头或改变方向) 给定整数 capacity 和一个数组 trips , trip[i] = [numPassengersi, fromi, toi] 表示第 i 次旅行有 numPassengersi 乘客,接他们和放他们的位置分别是 fromi 和 toi 。这些位置是从汽车的初始位置向东的公里数。 当且仅当你可以在所有给定的行程中接送所有乘客时,返回 true,否则请返回 false。 示例 : 输入:trips = [[2,1,5],[3,3,7]], capacity = 4 输出:false ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:4:1","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"解析 旅客的上车和下车就相当于数组的区间加减;只要结果数组中的元素都小于 capacity,就说明可以不超载运输所有旅客。 题目转化为差分数组,差分数组的长度即车站区间的个数,为1001。result 数组的值即为每段路程车上的人数。图解如下: 代码实现: class Solution { private int[] diff; private int size = 1005; public boolean carPooling(int[][] trips, int capacity) { diff = new int[size]; for(int i=0; i\u003ctrips.length; i++){ increment(trips[i][1],trips[i][2]-1,trips[i][0]); } return result(capacity); } public void increment(int from, int to, int val){ diff[from] += val; if(to \u003c 1000){ diff[to+1] -= val; } } public boolean result(int capacity){ int[] result = new int[size]; result[0] = diff[0]; if(result[0] \u003e capacity) return false; // 客⻋⾃始⾄终都不应该超载 for(int i=1; i\u003c=1000; i++){ result[i] = result[i-1] + diff[i]; if(result[i] \u003e capacity){ return false; } } return true; } } 参考资料: https://labuladong.github.io/algo/ ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:4:2","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"前缀和主要适⽤的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。 ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:0:0","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"1 一维数组中的前缀和 ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:1:0","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 303. 区域和检索 - 数组不可变 给定一个整数数组 nums,计算索引 left 和 right (包含 left 和 right)之间的 nums 元素的 和 ,其中 left \u003c= right,实现 NumArray 类: NumArray(int[] nums) 使用数组 nums 初始化对象 int sumRange(int i, int j) 返回数组 nums 中索引 left 和 right 之间的元素的 总和 ,包含 left 和 right 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] ) 示例: 输入: [\"NumArray\", \"sumRange\", \"sumRange\", \"sumRange\"] [[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]] 输出: [null, 1, -1, -3] 解释: NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]); numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3) numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1)) numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1)) ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:1:1","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"解析 不使用前缀和的做法: class NumArray { private int[] nums; public NumArray(int[] nums) { this.nums=nums; } public int sumRange(int left, int right) { int sum=0; for(int i=left;i\u003c=right;i++){ sum+=nums[i]; } return sum; } } 可以达到效果,但是效率很差,因为 sumRange ⽅法会被频繁调⽤,⽽它的时间复杂度是 O(N),其中 N 代表 nums 数组的⻓度。使⽤前缀和后, sumRange 函数的时间复杂度降为 O(1),避免使用 for 循环。 前缀和原理: 代码实现: class NumArray { // 前缀和数组 private int[] sum; public NumArray(int[] nums) { sum=new int[nums.length+1]; sum[0]=0; // 计算 nums 的累加和 for(int i=1;i\u003c=nums.length;i++) sum[i]=sum[i-1]+nums[i-1]; } public int sumRange(int left, int right) { return sum[right+1]-sum[left]; } } ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:1:2","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"延伸 这个技巧在⽣活中运⽤也挺⼴泛的,⽐⽅说,你们班上有若⼲同学,每个同学有⼀个期末考试的成绩(满分 100 分),那么请你实现⼀个 API,输⼊任意⼀个分数段,返回有多少同学的成绩在这个分数段内。 那么,你可以先通过计数排序的⽅式计算每个分数具体有多少个同学,然后利⽤前缀和技巧来实现分数段查 询的 API: int[] scores; // 存储着所有同学的分数 // 试卷满分 100 分 int[] count = new int[100 + 1] // 记录每个分数有⼏个同学 for (int score : scores) count[score]++ // 构造前缀和 for (int i = 1; i \u003c count.length; i++) count[i] = count[i] + count[i-1]; // 利⽤ count 这个前缀和数组进⾏分数段查询 ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:1:3","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"2 二维数组中的前缀和 ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:2:0","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 304. 二维区域和检索 - 矩阵不可变 给定一个二维矩阵 matrix,计算其子矩形范围内元素的总和,该子矩阵的 左上角 为 (row1, col1) ,右下角 为 (row2, col2) 。实现 NumMatrix 类: NumMatrix(int[][] matrix) 给定整数矩阵 matrix 进行初始化 int sumRegion(int row1, int col1, int row2, int col2) 返回 左上角 (row1, col1) 、右下角 (row2, col2) 所描述的子矩阵的元素 总和 。 ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:2:1","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"解析 和⼀维数组中的前缀和类似,我们可以维护⼀个⼆维 sum 数组,专⻔记录以原点为顶点的矩阵的元素之和,就可以⽤⼏次加减运算算出任何⼀个⼦矩阵的元素和,典型的 “空间换时间”。思路如图所示: 代码实现: class NumMatrix { // sum[i][j] 记录 matrix 中⼦矩阵 [0, 0, i-1, j-1] 的元素和 private int[][] sum; public NumMatrix(int[][] matrix) { int row=matrix.length; int column=matrix[0].length; sum=new int[row+1][column+1]; // 构造前缀和矩阵 for(int i=1;i\u003c=row;i++){ for(int j=1;j\u003c=column;j++){ // 计算每个矩阵 [0, 0, i, j] 的元素和 sum[i][j]=sum[i][j-1]+sum[i-1][j]-sum[i-1][j-1]+matrix[i-1][j-1]; } } } public int sumRegion(int row1, int col1, int row2, int col2) { return sum[row2+1][col2+1]-sum[row2+1][col1]-sum[row1][col2+1]+sum[row1][col1]; } } 参考资料: https://labuladong.github.io/algo/ ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:2:2","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["C++"],"content":"1 类\u0026对象 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:0:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.1 成员函数 成员函数可以定义在类定义内部: class Box { public: double length; // 长度 double breadth; // 宽度 double height; // 高度 double getVolume(void) { return length * breadth * height; } }; 也可以在类的外部使用范围解析运算符 :: 定义该函数。 class Box { public: double length; // 长度 double breadth; // 宽度 double height; // 高度 double getVolume(void);// 返回体积 }; double Box::getVolume(void) { return length * breadth * height; } 在 :: 运算符之前必须使用类名。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:1:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.2 类访问修饰符 关键字 public、private、protected 称为访问修饰符。 一个类可以有多个 public、protected 或 private 标记区域。每个标记区域在下一个标记区域开始之前或者在遇到类主体结束右括号之前都是有效的。成员和类的默认访问修饰符是 private。 class Base { public: // 公有成员 protected: // 受保护成员 private: // 私有成员 }; ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:2:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"public(公有成员) 公有成员在程序中类的外部是可访问的。您可以不使用任何成员函数来设置和获取公有变量的值 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:2:1","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"private(私有成员) 私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。默认情况下,类的所有成员都是私有的。 一般会在私有区域定义数据,在公有区域定义相关的函数,以便在类的外部也可以调用这些函数。 #include \u003ciostream\u003eusing namespace std; class Box { public: void setWidth( double wid ); double getWidth( void ); private: double width; }; // 成员函数定义 double Box::getWidth(void) { return width ; } void Box::setWidth( double wid ) { width = wid; } // 程序的主函数 int main( ) { Box box; // 不使用成员函数设置宽度 // box.width = 10.0; // Error: 因为 width 是私有的 box.setWidth(10.0); // 使用成员函数设置宽度 return 0; } ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:2:2","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"protected(受保护成员) **protected(受保护)**成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:2:3","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"继承中的特点 有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。 **public 继承:**基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private **protected 继承:**基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private **private 继承:**基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private 但无论哪种继承方式,上面两点都没有改变: private 成员只能被本类成员(类内)和友元访问,不能被派生类访问; protected 成员可以被派生类访问。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:2:4","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.3 类构造函数\u0026析构函数 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:3:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"构造函数 类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。 构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。 class Line { public: Line(); // 这是构造函数 private: double length; }; // 成员函数定义,包括构造函数 Line::Line(void) { cout \u003c\u003c \"Object is being created\" \u003c\u003c endl; } // 成员函数定义,包括构造函数 Line::Line( double len) { cout \u003c\u003c \"Object is being created, length = \" \u003c\u003c len \u003c\u003c endl; length = len; } ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:3:1","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"使用初始化列表来初始化字段 // 相当于上面带参的构造函数 Line::Line( double len): length(len) { cout \u003c\u003c \"Object is being created, length = \" \u003c\u003c len \u003c\u003c endl; } 假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,同理地,您可以使用上面的语法,只需要在不同的字段使用逗号进行分隔。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:3:2","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"析构函数 类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。 析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。 Line::~Line(void) { cout \u003c\u003c \"Object is being deleted\" \u003c\u003c endl; } ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:3:3","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.4 拷贝构造函数 它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于: 通过使用另一个同类型的对象来初始化新创建的对象。 复制对象把它作为参数传递给函数。 复制对象,并从函数返回这个对象。 如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。拷贝构造函数的最常见形式如下: classname (const classname \u0026obj) { // 构造函数的主体 } 在这里,obj 是一个对象引用,该对象是用于初始化另一个对象的。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:4:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.5 友元函数 类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。 友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。 如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend。 class Box { public: friend void printWidth( Box box ); }; 声明类 ClassTwo 的所有成员函数作为类 ClassOne 的友元,需要在类 ClassOne 的定义中放置如下声明: friend class ClassTwo; ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:5:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.6 this指针 this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。 友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:6:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.7 静态成员 当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。 我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化。 class Box { public: static int objectCount; ...... }; // 初始化类 Box 的静态成员 int Box::objectCount = 0; int main(void) { ...... // 输出对象的总数 cout \u003c\u003c \"Total objects: \" \u003c\u003c Box::objectCount \u003c\u003c endl; ...... } ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:7:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"静态成员函数 静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。 静态成员函数与普通成员函数的区别: 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。 普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:7:1","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"11 字符串 C风格字符串 字符串实际上是使用 null 字符 \\0 终止的一维字符数组。 char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\\0'}; char site[] = \"RUNOOB\"; 字符串相关函数: strcpy(s1, s2); 复制字符串 s2 到字符串 s1。 strcat(s1, s2); 连接字符串 s2 到字符串 s1 的末尾。连接字符串也可以用 + 号。 strlen(s1); 返回字符串 s1 的长度。 strcmp(s1, s2); 如果 s1 和 s2 是相同的,则返回 0;如果 s1\u003cs2 则返回值小于 0;如果 s1\u003es2 则返回值大于 0。 strchr(s1, ch); 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。 strstr(s1, s2); 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。 String类 string str1 = \"runoob\"; string str2 = \"google\"; string str3; int len ; // 连接 str1 和 str2 str3 = str1 + str2; // 连接后,str3 的总长度为12! len = str3.size(); ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/:1:0","tags":["C++"],"title":"C++基础语法(三)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["C++"],"content":"12 指针 指针声明 int *ip; /* 一个整型的指针 */ double *dp; /* 一个 double 型的指针 */ float *fp; /* 一个浮点型的指针 */ char *ch; /* 一个字符型的指针 */ 所有指针的值的实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,都是一样的,都是一个代表内存地址的长的十六进制数。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。 Null 指针 在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。 int *ptr = NULL; // ptr的值是0 指针的算术运算 假设 ptr 是一个指向地址 1000 的整型指针,是一个 32 位的整数。 🟡 递增一个指针 ptr++; 在执行完上述的运算之后,ptr 将指向位置 1004,因为 ptr 每增加一次,它都将指向下一个整数位置,即当前位置往后移 4 个字节。这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置。如果 ptr 指向一个地址为 1000 的字符,上面的运算会导致指针指向位置 1001,因为下一个字符位置是在 1001。 变量指针可以递增,而数组不能递增,因为数组是一个常量指针。 int var[MAX] = {10, 100, 200}; for (int i = 0; i \u003c MAX; i++) { *var = i; // 这是正确的语法 var++; // 这是不正确的,数组指针为常量 } 🔵 指针的比较 指针可以用关系运算符进行比较,如 ==、\u003c 和 \u003e。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。 指向指针的指针 当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。 int var = 300; int *ptr; int **pptr; ptr = \u0026var; // 获取 var 的地址 pptr = \u0026ptr; // 使用运算符 \u0026 获取 ptr 的地址 cout \u003c\u003c \"var 值为 :\" \u003c\u003c var \u003c\u003c endl; // 3000 cout \u003c\u003c \"*ptr 值为:\" \u003c\u003c *ptr \u003c\u003c endl; // 3000 cout \u003c\u003c \"**pptr 值为:\" \u003c\u003c **pptr \u003c\u003c endl; // 3000 传递指针给函数 从函数返回指针 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/:2:0","tags":["C++"],"title":"C++基础语法(三)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["C++"],"content":"13 引用 引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。 C++ 引用 vs 指针 不存在空引用。引用必须连接到一块合法的内存。 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。 引用必须在创建时被初始化。指针可以在任何时间被初始化。 创建引用 \u0026 读作引用。 int i; // 声明简单的变量 // r 是一个初始化为 i 的整型引用 int\u0026 r = i; // 声明引用变量 i = 5; cout \u003c\u003c \"Value of i : \" \u003c\u003c i \u003c\u003c endl; // 5 cout \u003c\u003c \"Value of i reference : \" \u003c\u003c r \u003c\u003c endl; // 5 把引用作为参数 把引用作为返回值 double vals[] = {10.1, 12.6, 33.1, 24.1, 50.0}; double\u0026 setValues(int i) { double\u0026 ref = vals[i]; return ref; // 返回第 i 个元素的引用,ref 是一个引用变量,ref 引用 vals[i] } int main () { setValues(1) = 20.23; // 改变第 2 个元素 cout \u003c\u003c vals[1] \u003c\u003c endl; // 由12.6变为20.23 return 0; } 当返回一个引用时,要注意被引用的对象不能超出作用域。所以返回一个对局部变量的引用是不合法的,但是,可以返回一个对静态变量的引用。 int\u0026 func() { int q; //! return q; // 在编译时发生错误 static int x; return x; // 安全,x 在函数作用域外依然是有效的 } ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/:3:0","tags":["C++"],"title":"C++基础语法(三)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["C++"],"content":"14 日期 \u0026 时间 C++ 标准库没有提供所谓的日期类型。C++ 继承了 C 语言用于日期和时间操作的结构和函数。为了使用日期和时间相关的函数和结构,需要在 C++ 程序中引用 \u003cctime\u003e 头文件。 相关数据类型 有四个与时间相关的类型:clock_t、time_t、size_t 和 tm。类型 clock_t、size_t 和 time_t 能够把系统时间和日期表示为某种整数。 结构类型 tm 把日期和时间以 C 结构的形式保存,tm 结构的定义如下: struct tm { int tm_sec; // 秒,正常范围从 0 到 59,但允许至 61 int tm_min; // 分,范围从 0 到 59 int tm_hour; // 小时,范围从 0 到 23 int tm_mday; // 一月中的第几天,范围从 1 到 31 int tm_mon; // 月,范围从 0 到 11 int tm_year; // 自 1900 年起的年数 int tm_wday; // 一周中的第几天,范围从 0 到 6,从星期日算起 int tm_yday; // 一年中的第几天,范围从 0 到 365,从 1 月 1 日算起 int tm_isdst; // 夏令时 }; 关于日期和时间的重要函数 time_t time(time_t *time); 该函数返回系统的当前日历时间,自 1970 年 1 月 1 日以来经过的秒数。如果系统没有时间,则返回 -1。 char *ctime(const time_t *time); 该返回一个表示当地时间的字符串指针,字符串形式 day month year hours:minutes:seconds year\\n\\0。 struct tm *localtime(const time_t *time); 该函数返回一个指向表示本地时间的 tm 结构的指针。 clock_t clock(void); 该函数返回程序执行起(一般为程序的开头),处理器时钟所使用的时间。如果时间不可用,则返回 -1。 char * asctime ( const struct tm * time ); 该函数返回一个指向字符串的指针,字符串包含了 time 所指向结构中存储的信息,返回形式为:day month date hours:minutes:seconds year\\n\\0。 struct tm *gmtime(const time_t *time); 该函数返回一个指向 time 的指针,time 为 tm 结构,用协调世界时(UTC)也被称为格林尼治标准时间(GMT)表示。 time_t mktime(struct tm *time); 该函数返回日历时间,相当于 time 所指向结构中存储的时间。 double difftime ( time_t time2, time_t time1 ); 该函数返回 time1 和 time2 之间相差的秒数。 size_t strftime(); 该函数可用于格式化日期和时间为指定的格式。 当前日期和时间 time_t now = time(0); // 基于当前系统的当前日期/时间 char* dt = ctime(\u0026now); // 把 now 转换为字符串形式 cout \u003c\u003c \"本地日期和时间:\" \u003c\u003c dt \u003c\u003c endl; // Sat Jan 8 20:07:41 2011 // 把 now 转换为 tm 结构 tm *gmtm = gmtime(\u0026now); dt = asctime(gmtm); cout \u003c\u003c \"UTC 日期和时间:\"\u003c\u003c dt \u003c\u003c endl; // Sat Jan 9 20:07:41 2011 使用结构 tm 格式化时间 // 基于当前系统的当前日期/时间 time_t now = time(0); tm *ltm = localtime(\u0026now); // 输出 tm 结构的各个组成部分 cout \u003c\u003c \"年: \"\u003c\u003c 1900 + ltm-\u003etm_year \u003c\u003c endl; cout \u003c\u003c \"月: \"\u003c\u003c 1 + ltm-\u003etm_mon\u003c\u003c endl; cout \u003c\u003c \"日: \"\u003c\u003c ltm-\u003etm_mday \u003c\u003c endl; ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/:4:0","tags":["C++"],"title":"C++基础语法(三)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["C++"],"content":"15 基本的输入输出 标准输出流(cout) cout 是与流插入运算符 « 结合使用的,« 运算符被重载来输出内置类型(整型、浮点型、double 型、字符串和指针)的数据项。 标准输入流(cin) cin 是与流提取运算符 » 结合使用的。 cin \u003e\u003e name \u003e\u003e age; cout \u003c\u003c \"您的名称是: \" \u003c\u003c name \u003c\u003c endl; cout \u003c\u003c \"您的年龄是: \" \u003c\u003c age \u003c\u003c endl; 标准错误流(cerr) cerr 对象是非缓冲的,且每个流插入到 cerr 都会立即输出。 char str[] = \"Unable to read....\"; cerr \u003c\u003c \"Error message : \" \u003c\u003c str \u003c\u003c endl; // 输出结果为 Error message : Unable to read.... 标准日志流(clog) char str[] = \"Unable to read....\"; clog \u003c\u003c \"Error message : \" \u003c\u003c str \u003c\u003c endl; // 输出结果为 Error message : Unable to read.... ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/:5:0","tags":["C++"],"title":"C++基础语法(三)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["C++"],"content":"16 数据结构 定义结构 struct Books { char title[50]; char author[50]; char subject[100]; int book_id; } book; 访问结构成员 使用成员访问运算符(.)。成员访问运算符是结构变量名称和我们要访问的结构成员之间的一个句号。 Books Book1; // 定义结构体类型 Books 的变量 Book1 // Book1 详述 strcpy( Book1.title, \"C++ 教程\"); strcpy( Book1.author, \"Runoob\"); strcpy( Book1.subject, \"编程语言\"); Book1.book_id = 12345; 结构作为函数参数 void printBook( struct Books book ) { cout \u003c\u003c \"书标题 : \" \u003c\u003c book.title \u003c\u003cendl; cout \u003c\u003c \"书作者 : \" \u003c\u003c book.author \u003c\u003cendl; cout \u003c\u003c \"书类目 : \" \u003c\u003c book.subject \u003c\u003cendl; cout \u003c\u003c \"书 ID : \" \u003c\u003c book.book_id \u003c\u003cendl; } 指向结构的指针 为了使用指向该结构的指针访问结构的成员,您必须使用 -\u003e 运算符. int main( ) { ...... printBook( \u0026Book1 ); // 通过传 Book1 的地址来输出 Book1 信息 ...... } // 该函数以结构指针作为参数 void printBook( struct Books *book ) { cout \u003c\u003c \"书标题 : \" \u003c\u003c book-\u003etitle \u003c\u003cendl; cout \u003c\u003c \"书作者 : \" \u003c\u003c book-\u003eauthor \u003c\u003cendl; cout \u003c\u003c \"书类目 : \" \u003c\u003c book-\u003esubject \u003c\u003cendl; cout \u003c\u003c \"书 ID : \" \u003c\u003c book-\u003ebook_id \u003c\u003cendl; } typedef 关键字 下面是一种更简单的定义结构的方式,您可以为创建的类型取一个\"别名\"。例如: typedef struct Books { char title[50]; char author[50]; char subject[100]; int book_id; }BO; BO Book1, Book2; 您可以使用 typedef 关键字来定义非结构类型,如下所示: typedef long int *pint32; pint32 x; // x是指向长整型 long int 的指针。 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/:6:0","tags":["C++"],"title":"C++基础语法(三)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["C++"],"content":"6 运算符 sizeof运算符 sizeof 运算符可用于获取类、结构、共用体和其他用户自定义数据类型的大小。 逗号运算符 整个逗号表达式的值为系列中最后一个表达式的值。从本质上讲,逗号的作用是将一系列运算按顺序执行。 逗号之前的自增表达式也会在逗号结束后执行! // 运行完结果:count=19,incr=10,var=20 var = (count=19, incr=10, count+1); // 结果:j=11,i=1010 j = 10; i = (j++, j+100, 999+j); 成员运算符 .(点)运算符和 -\u003e(箭头)运算符用于引用类、结构和共用体的成员。访问结构的成员时使用点运算符,而通过指针访问结构的成员时,则使用箭头运算符。例如,假设有下面的结构: struct Employee { char first_name[16]; int age; } emp; 点运算符: strcpy(emp.first_name, \"zara\"); 箭头运算符: // p_emp 是一个指针,指向类型为 Employee 的对象 strcpy(p_emp-\u003efirst_name, \"zara\"); 强制转换运算符 double a = 21.09399; int c = (int) a; // 结果为21 指针运算符 取地址运算符 \u0026:返回操作数的内存地址。 间接寻址运算符 *:返回操作数所指定地址的变量的值。 int var = 3000; int *ptr; int val; ptr = \u0026var; // 获取 var 的地址 val = *ptr; // 获取 ptr 的值 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:0","tags":["C++"],"title":"C++基础语法(二)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["C++"],"content":"7 循环 基于范围的for循环: int my_array[5] = {1, 2, 3, 4, 5}; // 每个数组元素乘于 2 for (int \u0026x : my_array) { x *= 2; cout \u003c\u003c x \u003c\u003c endl; } ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/:2:0","tags":["C++"],"title":"C++基础语法(二)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["C++"],"content":"8 switch语句 switch 语句中的 expression 必须是一个整型或枚举类型,或者是一个 class 类型,其中 class 有一个单一的转换函数将其转换为整型或枚举类型。 不是每一个 case 都需要包含 break。如果 case 语句不包含 break,控制流将会 继续 后续的 case,直到遇到 break 为止。 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/:3:0","tags":["C++"],"title":"C++基础语法(二)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["C++"],"content":"9 函数 函数声明 int max(int num1, int num2); // 在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明: int max(int, int); 当您在一个源文件中定义函数且在另一个文件中调用函数时,函数声明是必需的。在这种情况下,您应该在调用函数的文件顶部声明函数。 函数参数 形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。当调用函数时,有三种向函数传递参数的方式: 传值调用 该方法把参数的实际值赋值给函数的形式参数。在这种情况下,修改函数内的形式参数对实际参数没有影响。 指针调用 把参数的地址复制给形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。 // 调用:swap(\u0026a,\u0026b) void swap(int *x, int *y) { int temp; temp = *x; /* 保存地址 x 的值 */ *x = *y; /* 把 y 赋值给 x */ *y = temp; /* 把 x 赋值给 y */ } 引用调用 把引用的地址复制给形式参数。在函数内,该引用用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。 // 调用:swap(a,b) void swap(int \u0026x, int \u0026y) { int temp; temp = x; /* 保存地址 x 的值 */ x = y; /* 把 y 赋值给 x */ y = temp; /* 把 x 赋值给 y */ } 参数的默认值 定义一个函数,可以为参数列表中后边的每一个参数指定默认值。当调用函数时,如果实际参数的值留空,则使用这个默认值。 int sum(int a, int b=20) { int result; result = a + b; return (result); } Lambda 函数与表达式 对匿名函数的支持,称为 Lambda 函数(也叫 Lambda 表达式)。 Lambda 表达式把函数看作对象。Lambda 表达式可以像对象一样使用,比如可以将它们赋给变量和作为参数传递,还可以像函数一样对其求值。Lambda 表达式具体形式如下: // 一般形式 [capture](parameters)-\u003ereturn-type{body} // 一般情况 [](int x, int y) -\u003e int { int z = x + y; return z + x; } // 无返回类型 [](int x, int y){ return x \u003c y ; } // 无参数 []{ ++global_x; } 在Lambda表达式内可以访问当前作用域的变量,这是Lambda表达式的闭包(Closure)行为。 与JavaScript闭包不同,C++变量传递有传值和传引用的区别。可以通过前面的[]来指定: [] // 沒有定义任何变量。使用未定义变量会引发错误。 [x, \u0026y] // x以传值方式传入(默认),y以引用方式传入。 [\u0026] // 任何被使用到的外部变量都隐式地以引用方式加以引用。 [=] // 任何被使用到的外部变量都隐式地以传值方式加以引用。 [\u0026, x] // x显式地以传值方式加以引用。其余变量以引用方式加以引用。 [=, \u0026z] // z显式地以引用方式加以引用。其余变量以传值方式加以引用。 另外有一点需要注意。对于[=]或[\u0026]的形式,lambda 表达式可以直接使用 this 指针。但是,对于[]的形式,如果要使用 this 指针,必须显式传入: [this]() { this-\u003esomeFunc(); }(); ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/:4:0","tags":["C++"],"title":"C++基础语法(二)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["C++"],"content":"10 数字 数学运算 引用数学头文件 \u003ccmath\u003e可以使用C++内置的数学函数。 double log(double); 该函数返回参数的自然对数。 double pow(double, double); 假设第一个参数为 x,第二个参数为 y,则该函数返回 x 的 y 次方。 double sqrt(double); 该函数返回参数的平方根。 int abs(int); 该函数返回整数的绝对值。 double fabs(double); 该函数返回任意一个浮点数的绝对值。 double floor(double); 该函数返回一个小于或等于传入参数的最大整数。 随机数 关于随机数生成器,有两个相关的函数。一个是 rand(),该函数只返回一个伪随机数。生成随机数之前必须先调用 srand() 函数。 下面是一个关于生成随机数的简单实例。实例中使用了 time() 函数来获取系统时间的秒数,通过调用 rand() 函数来生成随机数: int main () { // 设置种子 srand( (unsigned)time( NULL ) ); // 生成实际的随机数 int j= rand(); return 0; } ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/:5:0","tags":["C++"],"title":"C++基础语法(二)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["C++"],"content":"11 数组 多维数组 int a[3][4] = { {0, 1, 2, 3} , /* 初始化索引号为 0 的行 */ {4, 5, 6, 7} , /* 初始化索引号为 1 的行 */ {8, 9, 10, 11} /* 初始化索引号为 2 的行 */ }; // 内部嵌套的括号是可选的,下面的初始化与上面是等同的: int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11}; 指向数组的指针 double *p; double runoobAarray[10]; // 把 p 赋值为 runoobAarray 的第一个元素的地址 p = runoobAarray; // *(runoobAarray + 4) 是一种访问 runoobAarray[4] 数据的合法方式 // *(runoobAarray + 4) == *(p+4) 传递数组给函数 C++ 传数组给一个函数,数组类型自动转换为指针类型,因而传的实际是地址。 如果您想要在函数中传递一个一维数组作为参数,您必须以下面三种方式来声明函数形式参数,这三种声明方式的结果是一样的,因为每种方式都会告诉编译器将要接收一个整型指针。同样地,您也可以传递一个多维数组作为形式参数。 形式参数是一个指针: void myFunction(int *param){} 形式参数是一个已定义大小的数组: void myFunction(int param[10]){} 形式参数是一个未定义大小的数组: void myFunction(int param[]){} 就函数而言,数组的长度是无关紧要的,因为 C++ 不会对形式参数执行边界检查。 从函数返回数组 C++ 不允许返回一个完整的数组作为函数的参数。但是,您可以通过指定不带索引的数组名来返回一个指向数组的指针。 // 要生成和返回随机数的函数 int * getRandom( ) { static int r[10]; // 设置种子 srand( (unsigned)time( NULL ) ); for (int i = 0; i \u003c 10; ++i) { r[i] = rand(); } return r; } ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/:6:0","tags":["C++"],"title":"C++基础语法(二)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["C++"],"content":"1 C++简介 C++ 完全支持面向对象的程序设计,包括面向对象开发的四大特性:封装、抽象、继承、多态 标准的 C++ 由三个重要部分组成: 核心语言,提供了所有构件块,包括变量、数据类型和常量,等等。 C++ 标准库,提供了大量的函数,用于操作文件、字符串等。 标准模板库(STL),提供了大量的方法,用于操作数据结构等。 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/:1:0","tags":["C++"],"title":"C++基础语法(一)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["C++"],"content":"2 数据类型 基本内置类型 bool,char,int,float,double,void 类型修饰符 signed,unsigned,short,long C++ 允许使用速记符号来声明无符号短整数或无符号长整数。您可以不写 int,只写单词 unsigned、short 或 long。 typedef 声明 可以使用 typedef 为一个已有的类型取一个新的名字。 typedef int feet; 枚举类型 如果一个变量只有几种可能的值,可以定义为枚举(enumeration)类型。所谓\"枚举\"是指将变量的值一一列举出来,变量的值只能在列举出来的值的范围内。 enum 枚举名{ 标识符[=整型常数], ... } 枚举变量; 默认情况下,第一个名称的值为 0,第二个名称的值为 1,第三个名称的值为 2,以此类推。但是,您也可以给名称赋予一个特殊的值,只需要添加一个初始值即可。 enum color { red, green, blue } c; c = blue; // red默认为0,blue默认为6 enum color { red, green=5, blue }; 变量声明 当使用多个文件且只在其中一个文件中定义变量时,可以使用 extern 关键字在任何地方声明一个变量。虽然您可以在 C++ 程序中多次声明一个变量,但变量只能在某个文件、函数或代码块中被定义一次。 #include \u003ciostream\u003eusing namespace std; // 变量声明 extern float f; ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/:2:0","tags":["C++"],"title":"C++基础语法(一)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["C++"],"content":"3 变量作用域 作用域是程序的一个区域,一般来说有三个地方可以定义变量: 在函数或一个代码块内部声明的变量,称为局部变量。 在函数参数的定义中声明的变量,称为形式参数。 在所有函数外部声明的变量,称为全局变量。 局部变量和全局变量的名称可以相同,但是在函数内,局部变量的值会覆盖全局变量的值。 当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动初始化。 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/:3:0","tags":["C++"],"title":"C++基础语法(一)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["C++"],"content":"4 常量 整数常量 前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。 后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long),可以大写也可以小写,U和L的顺序任意。 浮点常量 当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。带符号的指数是用 e 或 E 引入的。 314159E-5L // 合法的 510E // 非法的:不完整的指数 210f // 非法的:没有小数或指数 .e55 // 非法的:缺少整数或分数 字符串常量 可以使用 “\\” 做分隔符,把一个很长的字符串常量进行分行。 定义常量 使用 #define 预处理器定义常量: #include \u003ciostream\u003eusing namespace std; #define NEWLINE '\\n' int main() { cout \u003c\u003c NEWLINE; return 0; } 使用 const 前缀声明指定类型的常量: int main() { const char NEWLINE = '\\n'; cout \u003c\u003c NEWLINE; return 0; } 注意:尽量把常量定义为大写字母形式。 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/:4:0","tags":["C++"],"title":"C++基础语法(一)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["C++"],"content":"5 存储类 static 存储类 使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。 static 用在类数据成员上时,会导致仅有一个该成员的副本被类的所有对象共享。 extern 存储类 当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。 mutable 存储类 thread_local 存储类 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/:5:0","tags":["C++"],"title":"C++基础语法(一)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["labuladong的算法秘籍"],"content":"1 数据结构的存储方式 ","date":"2022-12-23","objectID":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/:1:0","tags":["labuladong","算法"],"title":"算法和刷题的框架思维","uri":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/"},{"categories":["labuladong的算法秘籍"],"content":"1.1 数据结构\u0026存储方式 数据的存储方式只有两种:数组(顺序存储)和链表(链式存储)。 【队列】【栈】 【图】:链表实现就是邻接表,二维数组实现就是邻接矩阵 【散列表】 【树】:数组实现就是堆(完全二叉树),链表实现就是普通二叉树 ","date":"2022-12-23","objectID":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/:1:1","tags":["labuladong","算法"],"title":"算法和刷题的框架思维","uri":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/"},{"categories":["labuladong的算法秘籍"],"content":"1.2 数组\u0026链表优缺点 数组: 紧凑连续存储,可以随机访问,通过索引快速找到对应元素,节约存储空间。 内存空间必须⼀次性分配够,扩容时需要重新分配空间,再把数据全部复制过去,时间复杂度 O(N) 插⼊和删除时间复杂度 O(N) 链表: 元素不连续,不存在数组的扩容问题; 如果知道某⼀元素的前驱和后驱,插入删除时间复杂度 O(1) 存储空间不连续,不能随机访问 每个元素必须存储指向前后元素位置的指针,会消耗相对更多的储存空间。 ","date":"2022-12-23","objectID":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/:1:2","tags":["labuladong","算法"],"title":"算法和刷题的框架思维","uri":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/"},{"categories":["labuladong的算法秘籍"],"content":"2 数据结构的基本操作 数据结构的基本操作:遍历+访问(增删查改),分为线性/非线性。 线性即for/while迭代,非线性即递归。 🟡 【数组遍历框架】 迭代 void traverse(int[] arr) { for (int i = 0; i \u003c arr.length; i++) { // 迭代访问 arr[i] } } 🟢 【链表遍历框架】 迭代/递归 /* 基本的单链表节点 */ class ListNode { int val; ListNode next; } void traverse(ListNode head) { for (ListNode p = head; p != null; p = p.next) { // 迭代访问 p.val } } void traverse(ListNode head) { // 递归访问 head.val traverse(head.next); } 🔵 【二叉树遍历框架】 递归 /* 基本的⼆叉树节点 */ class TreeNode { int val; TreeNode left, right; } void traverse(TreeNode root) { traverse(root.left); traverse(root.right); } 🟣 【N叉树遍历框架】 递归 /* 基本的 N 叉树节点 */ class TreeNode { int val; TreeNode[] children; } void traverse(TreeNode root) { for (TreeNode child : root.children) traverse(child); } N 叉树的遍历又可以扩展为图的遍历,因为图就是好几个 N 叉棵树的结合体。图是可能出现环的,用个布尔数组 visited 做标记就行。 ","date":"2022-12-23","objectID":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/:2:0","tags":["labuladong","算法"],"title":"算法和刷题的框架思维","uri":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/"},{"categories":["labuladong的算法秘籍"],"content":"3 算法刷题指南 ✅ 先学习像数组、链表这种基本数据结构的常用算法,比如单链表翻转,前缀和数组,二分搜索等。 因为这些算法属于会者不难难者不会的类型,难度不大,学习它们不会花费太多时间。而且这些小而美的算法经常让你大呼精妙,能够有效培养你对算法的兴趣。 ✅ 学会基础算法之后,不要急着上来就刷回溯算法、动态规划这类笔试常考题,而应该先刷⼆叉树。因为⼆叉树是最容易培养框架思维的,而且⼤部分算法技巧,本质上都是树的遍历问题。 ✅ 刷题试着从框架上看问题,而不要纠结于细节问题。 纠结细节问题,就比如纠结 i 到底应该加到 n 还是加到 n - 1,这个数组的⼤⼩到底应该开 n 还是 n + 1? 参考资料: https://labuladong.github.io/algo/ ","date":"2022-12-23","objectID":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/:3:0","tags":["labuladong","算法"],"title":"算法和刷题的框架思维","uri":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/"},{"categories":["Mysql"],"content":"7 并发控制与事务的隔离级别 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:0:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"7.1 并发控制与事务的隔离级别 并发操作可能产生的数据不一致性 数据库是共享资源,允许多个用户同时访问同一数据库,特别是在互联网应用成为主流的当下,高可用性、高并发是所有应用追求的目标。但并发操作不加控制,便会产生数据的不一致性。 并发操作可能带来的数据不一致性包括: 丢失修改(lost update) 读脏数据(dirty read) 不可重复读(non-repeatable read) 幻读(phantom read) 为解决上述不一致性问题,DBMS设计了专门的并发控制子系统,采用封锁机制进行并发控制,以保证事务的隔离性和一致性(事务是并发控制的基本单位)。 但事务的隔离程度越高,固然一致性–或者説数据的正确性越有保障,但并发度就会越低。很多时候,需要在一致性和并发度间进行取舍,从而就生产了事务的隔离级别的概念。 隔离级别越高,一致性程度越高,并发度越低。反之,隔离级别越低,并发度越高,但代价是会出现某些数据不一致现象。 低隔离级别可以支持更高的并发处理,同时占用的系统资源更少,但可能产生数据不一致的情形也更多一些。 查询事务的隔离级别 可用以下语句查询MySQL的事务隔离级别: select @@GLOBAL.transaction_isolation, @@transaction_isolation; 其中,@@GLOBAL.transaction_isolation全局变量,@@transaction_isolation为本会话期内的变量。通常通过重设该变量的值以改变隔离级别。 上述两个变量的缺省值均为:REPEATABLE-READ,即可重复读。 设置事务的隔离级别 以下语句设置事务的隔离级别为可读未提交(read uncommitted): set session transaction isolation level read uncommitted; 如需设置为其它级别,只需替换最后的隔离级别即可。 不同的事务隔离级别意味着不同的封锁协议,程序员只需设置事务的隔离级别即可,其它的交给DBMS并发子系统处理。 不过,MySQL也有lock tables和unlock tables语句,可以直接锁表,另外,MySQL还支持在select语句中使用for share或for update短语主动申请S锁或X锁(只到事务结束才释放)。这样,即使在隔离级别为read uncommitted的情形下,仍有机会保证可重复读,相关内容请参阅MySQL官方文档。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:1:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"7.2 读脏 读脏 读脏(dirty read),或者又叫脏读,是指一个事务(t1)读取到另一个事务(t2)修改后的数据,后来事务t2又撤销了本次修改(即事务t2以roll back结束),数据恢复原值。这样,事务t1读到的数据就与数据库里的实际数据不一致,这样的数据被称为“脏”数据,意即不正确的数据。 读脏产生的原因 显然,产生读脏的原因,是事务t1读取数据时,修改该数据的事务t2还没有结束(commit或roll back,统称uncommitted),且t1读取的时间点又恰在t2修改该数据之后。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:2:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"7.3 不可重复读 不可重复读 不可重复读(unrepeatable read),是指一个事务(t1)读取到某数据后,另一个事务(t2)修改了该,事务t1并未修改该数据,但当t1再次读取该数据时,发现两次读取的结果不一样。 产生不可重复读的原因 显然,不可重复读产生的原因,是事务t1的两次读取之间,有另一个事务修改了t1读取的数据。 根据第一关介绍的基本知识,有两种隔离级别都有可能发生不可重复读。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:3:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"7.4 幻读 幻读(phantom read) 幻读定义其实是有些争议的,在某些文献中,幻读被归为不可重复读(unrepeatable read)中的一类,而另一些则把它与不可重复读区分开来:幻读是指一个事务(t1)读取到某数据后,另一个事务(t2)作了insert或delete操作,事务t1再次读取该数据时,魔幻般地发现数据变多了或者变少了(记录数量不一致);而不可重复读限指事务t2作了update操作,致使t1的两次读操作读到的结果(数据的值)不一致。 产生幻读的原因 显然,幻读产生的原因,是事务t1的两次读取之间,有另一个事务insert或delete了t1读取的数据集。 除了最高级别serializable(可串行化)以外的任何隔离级别,都有可能发生幻读。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:4:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"7.5 MySQL对共享锁与锁的支持 通过设置不同的隔离级别,以实现不同的一致性与并发度的需求是较通常的作法。但MySQL也提供了主动加锁的机制,使得在较低的隔离级别下,通过加锁,以实现更高级别的一致性。 MySQL的select语句支持for share和for update短语,分别表示对表加共享(Share)锁和写(write)锁,共享锁也叫读锁,写锁又叫排它锁。 下面这条语句,会对表t1加共享锁: select * from t1 for share; 如果select语句涉及多张表,还可分别对不同的表加不同的锁,比如: select * from t1,t2 for share of t1 for update of t2; 加锁短语总是select语句的最后一个短语(复杂的select语句可能有where,group by, having, order by等短语);不管share还是update锁,都是在事务结束时才释放。当然,锁行为会降低并发度。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:5:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"7.6 可串行化 多个事务并发执行是正确的,当且仅当其结果与按某一次序串行地执行这些事务时的结果相同。两个事务t1,t2并发执行,如果结果与t1→t2串行执行的结果相同,或者与t2→t1串行执行的结果相同,都是正确的(可串行化的)。 如果将事务的隔离级别设置为serializable,则这些事务并发执行,无论怎么调度都会是可串行化的。但这种隔离级别会大大降低并发度,在实践中极小使用。MySQL默认的隔离级别为repeatable read,有的DBMS默认为read committed。 8 介质故障与数据库恢复 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:6:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"8.1 MySQL的恢复机制 和大多数DBMS一样,MySQL利用备份、日志文件实现恢复。 具体理论知识在此不详细介绍。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:7:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"8.2 MySQL的备份与恢复工具 MySQL提供了以下工具: 逻辑备份工具:mysqldump 物理备份工具:mysqlbackup(仅限商用版) 日志工具:mysqlbinlog 还原工具:mysql 管理工具:mysqladmin ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:8:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"mysqldump 逻辑备份工具,它产生一系的SQL语句,执行这些语句可以重建原数据库的所有对象和数据。缺省输出是控制台,可以通过重定向符号,将其产生的SQL语句集合存入到某个文件。 mysqldump可以备份服务器上的全部数据库,也可以指定某些数据库,或者一个数据库中的某些表。 mysqldump -h127.0.0.1 -uroot -p123123 [options] --databases db_name –databases 参数用指定数据库名,后面可跟一个或多个数据库的名字,多个数据库名间用空格隔开。 mysqldump命令行工具还可以带若干参数,可选的参数多达几十个,详见官方参考手册。这里只介绍一个: –flush-logs 刷MySQL日志,即重新开始一个日志文件。 重新开始一个新的日志文件,对未来确定哪些日志更有用很有帮助。通常海量备份前的日志文件,其重要性会降低许多,因为有备份在手,除非备份文件出故障,你可能不再需要使用之前的日志文件。 mysqlbackup mysqlbackup是MySQL的物理备份工具,只有付费的商用企业版才有。 mysql mysql是MySQL最重要的客户端管理工具,通常用户都是通过mysql登录到MySQL服务器,进行各种操作。此外,还可以直接通过它执行SQL脚本,还原或创建新库。 mysql -h127.0.0.1 -uroot -p12313 \u003c mydb.sql 这样会直接执行mydb.sql的脚本。通过mysqldump备份出来的脚本文件,可以用该方法直接用来恢复原数据库。 mysqladmin mysqladmin是MySQL服务器的管理工具,一般用于配置服务器,也可以用来创建或删除数据库: mysqladmin [options] command [command-arg] [command [command-arg]] 常用的command(执行命令)有: create db_name 创建数据库 drop db_name 删除数据库 flush-logs 刷日志 flush-tables 刷表,所有表数据写入磁盘盘 kill id,id,… 杀死某些进程 password new_password 修改(登录者的)登录密码 ping 检查服务器是否可用 status 显示服务器状态 variables 显示各配置参数的值。 mysqlbinlog mysqlbinlog是MySQL的日志管理工具。在需要手工介入的故障恢复中,该工具必不可少。当然,平常也可以用它查看日志。 mysqlbinlog mysql-bin.000983 上面的例子,用来查看日志文件mysql-bin.000983。MySQL的日志文件具有相同的前缀,后面的数字是日志文件的顺序。这个前缀是可配置的。比如,也可能是binlog.*。 执行日志文件会导致日志所记录的事件重新做一遍,这样可以恢复一个给定时间段的数据,恢复的方法如下: mysqlbinlog [option] binlog_files | mysql -u root -p 介质故障的恢复通常需要把最近一次备份后所有的日志文件全部列上。 mysqlbinlog也支持几十个可选的参数,比如: disable-log-bin 在通过日志恢复数据库期间不再写日志 no-defaults 不使用MySQL默认的设置。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:8:1","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"5 用户自定义函数 函数其实有多种,比如标量函数(仅返回一个值)和表函数(返回结果是表),语法也各不相同。这里,我们仅给出一个简化的创建标量函数的语法: create function function_name([para data_type[,...]]) returns data_type begin function_body; return expression; end function_name:函数名; para:参数名; data_type:参数的数据类型; 一个函数可以没有参数,也可以有多个。多参数间用逗号分隔。 function_body:函数体。即由合法的SQL语句组成的程序段。 expression:函数返回值,可以是常量、表达式,甚至是一条select语句查询的值(必须保证结果唯一);该值类型应与returns短语定义的类型相同。 函数一旦定义,就可以像内部函数一样使用,比如出现在select列表、表达式、以及where子句的条件中。 MySQL的函数定义与存储过程的定义一样,在定义函数之前要用“delimiter 界符”语句指定函数定义的结束界符,并在函数定义后,再次使用“delimiter 界符”语句恢复MySQL语句的界符(分号)。 6 安全性控制 与大多数商用DBMS一样,MySQL采用自主存取控制(DAC)机制进行安全性管理。通过用户,数据对象,权限,授权,收回权限等要素进行存取控制。另外,为了方便批量授权给同一类用户,引入了角色。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/:0:0","tags":["Mysql"],"title":"Mysql数据库基本语法(三)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["Mysql"],"content":"6.1 用户(User) MySQL创建用户的语句: create user 用户名 identified by 用户登录密码; 通常用户名可包含域名,限定用户在该域名内登录再有效。例: CREATE USER 'jeffrey'@'localhost' IDENTIFIED BY 'password'; 该语句创建用户jeffrey,密码为’password',仅限在MySQL服务器本机上登录才有效。用户名与域合起来,被称为账户(account)。 注意不要写成:‘jeffrey@localhost’,它代表账户: ‘jeffrey@localhost’@'%' 意即用户名为jefrrey@localhost,在任何机器上登录都有效。两者的含义完全不同。 drop user语句可删除用户。用户被删除时,该用户拥有的权限自动被收回。 alter user语句可重置用户密码: ALTER USER user IDENTIFIED BY 'new_password'; MySQL在安装时,初始用户名为root,此为系统管理员用户,其余用户均由root创建,并授权。经授权的用户也可以创建用户。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/:1:0","tags":["Mysql"],"title":"Mysql数据库基本语法(三)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["Mysql"],"content":"6.2 权限 MySQL常用的权限有: all: 所有权限(grant option除外) alter: alter table权限 alter routine: alter 存储过程 create: create database/table create role: create role create foutine: create 存储过程和函数 create user: create/alter/rename/drop user create view: create view delete: delete语句 drop: drop database/table drop role: drop role execute: 调用存储过程或函数 index:create/drop index insert: insert语句 select: select语句 trigger: 触发器相关操作 update: update语句 等。 select,update,insert,delete还可以用在列上,如select(c_id),update(b_balance)等。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/:2:0","tags":["Mysql"],"title":"Mysql数据库基本语法(三)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["Mysql"],"content":"6.3 角色 角色是权限的集合。如果有一组人(承担相同职责的小组,或者説小组成员扮演相同的角色)应该被授予一组相同的权限,不妨创建一个角色,将那组权限授予该角色,然后再将角色授予该组的每个成员。这比一个个地给每个组员授予一批权限要方便得多。 创建角色的语句: CREATE ROLE [IF NOT EXISTS] role [, role ] ... 一次可以创建多个角色。 删除角色: DROP ROLE [IF EXISTS] role [, role ] ... 角色被删除后,拥有该角色的用户立即失去角色定义的权限组合。不过,如果用户同时拥有多个角色,两个角色代表的权限集合如果有交集,则该用户仍拥有交集代表的权限。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/:3:0","tags":["Mysql"],"title":"Mysql数据库基本语法(三)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["Mysql"],"content":"6.4 GRANT授权语句 以下语句授予权限给用户或角色: grant 权限[,权限] ... on 数据库对象 to user|role,[user|role]... [with grant option] 可以同时将多个权限授予多个用户或角色。 with grant option表示被授权用户可以传播权限,即授权该用户将其拥有的权限(之前获得的权限,通过本语句获得的权限,以及今后获得的权限)再授予其它用户。 以下语句授予角色所代表的权限集给用户或角色: GRANT role [, role] ... TO user_or_role [, user_or_role] ... [WITH ADMIN OPTION] 总之,GRANT语句可以将权限或角色(权限集合)授予用户或角色。但是不能将权限和角色混合授予用户(或角色)。不过,你可以分开用两条不同的GRANT语句来实现:直接授权语句有关键词ON,间接授权(角色代表的权限集合)语句不带ON关键词。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/:4:0","tags":["Mysql"],"title":"Mysql数据库基本语法(三)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["Mysql"],"content":"6.5 REVOKE收回权限语句 以下语句将对象的权限从用户或角色手中收回: revoke 权限[,权限]... on 数据库对象 from user|role[,user|role]... 下列语句把role所代表的权限集合从用户或角色中收回: REVOKE role [, role ] ... FROM user_or_role [, user_or_role ] ... 如果用户本身拥有多个角色所代表的权限集合,而这些集合存在交集,收回其中部分角色代表的权限集后,用户可能仍拥有那个角色所代表的部分权限(交集代表的那部分权限)。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/:5:0","tags":["Mysql"],"title":"Mysql数据库基本语法(三)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["Mysql"],"content":"3存储过程与事务 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:0:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"3.1 使用流程控制语句的存储过程 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"变量的定义与赋值 用declare语句定义变量,并赋予默认值或初始值,未赋默认值则初始值为null: DECLARE var_name [, var_name] ... type [DEFAULT value] 用set语句给变量赋值,set语句还可以设置许多MySQL的配置参数。 SET variable = expr [, variable = expr] 通过select语句给变量赋值,select语句可以带复杂的where,group by,having等短语。 select col into var_name from table; #将table表中的col列值赋给变量 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:1","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"复合语句与流程控制语句 复合语句BEGIN…END BEGIN [statement_list] END; if语句 IF search_condition THEN statement_list [ELSEIF search_condition THEN statement_list] ... [ELSE statement_list] END IF; while语句 WHILE search_condition DO statement_list END WHILE; ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:2","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"存储过程的定义 存储过程是一种在数据库中存储复杂程序,以便外部程序调用的一种数据库对象。 存储过程是为了完成特定功能的 SQL 语句集,经编译创建并保存在数据库中,用户可通过指定存储过程的名字并给定参数(需要时)来调用执行。 存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用,即具有名字的一段代码,用来完成一个特定的功能。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:3","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"存储过程的创建和查询 创建存储过程: create procedure 存储过程名(参数) 每个存储的程序都包含一个由 SQL 语句组成的主体。此语句可能是由以分号(;)字符分隔的多个语句组成的复合语句。例如: CREATE PROCEDURE proc1() BEGIN SELECT * FROM user; END; MySQL 本身将分号识别为语句分隔符,因此必须临时重新定义分隔符以使 MySQL 将整个存储的程序定义传递给服务器。 要重新定义 MySQL 分隔符,请使用 delimiter命令。使用 delimiter 首先将结束符定义为//,完成创建存储过程后,使用//表示结束,然后将分隔符重新设置为分号(;): DELIMITER // CREATE PROCEDURE proc1() BEGIN SELECT * FROM user; END // DELIMITER ; /也可以换成其他符号,例如$; 执行存储过程: call 存储过程名 存储过程的参数: IN:输入参数,也是默认模式,表示该参数的值必须在调用存储过程时指定,在存储过程中修改该参数的值不能被返回; OUT:输出参数,该值可在存储过程内部被改变,并可返回; INOUT:输入输出参数,调用时指定,并且可被改变和返回。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:4","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"存储过程的查询和删除 查询存储过程: SHOW PROCEDURE STATUS WHERE db='数据库名'; 查看存储过程的详细定义信息: SHOW CREATE PROCEDURE 数据库.存储过程名; 删除存储过程: DROP PROCEDURE [IF EXISTS] 数据库名.存储过程名; ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:5","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"3.2 使用游标的存储过程 SQL操作都是面向集合的,即操作的对象以及运算的结果均为集合,但有时候,我们需要一行一行地处理数据,这就需要用到游标(CURSOR),它相当于一个存储于内存的带有指针的表,每次可以存取指针指向的一行数据,并将指针向前推进一行。游标的数据通常是一条查询语句的结果。对游标的操作一般要用循环语句,遍历游标的每一行数据,并将该行数据读至变量,再根据变量的值进行所需要的其它操作。 游标的特点: 不可滚动。即只能从前往后遍历游标数据(即从第1行到最后一行),不能反向遍历,不能跳跃遍历,不能直接访问中间的某一行。 只读。游标里的数据只能读取,不能修改。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:2:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"游标的定义与使用 1.DECLARE语句 DECLARE定义的顺序要求:变量→右边→特情处理 变量用来存储从游标读取的数据,根据编程逻辑的需要,可能还要定义其它变量;游标用来存储SELECT语句读取的数据集;当某些特定情形出现时,会自动触发对应的特情处理程序。 定义变量: DECLARE var_name [, var_name] ... type [DEFAULT value] 定义游标: DECLARE cursor_name CURSOR FOR select_statement 任何合法的select语句(不能带INTO短语),都可以定义成游标。此后可用FETCH语句读取这个select语句查询到的数据集中的一行数据。 注意游标必须定义在变量之后,特情处理程序之前。 一个存储过程可义定义多个游标,但不能同名。 定义特情处理的例子: DECLARE handler_action HANDLER FOR condition_value [, condition_value] ... statement handler_action: { CONTINUE | EXIT } condition_value: { mysql_error_code | SQLSTATE [VALUE] sqlstate_value | condition_name | SQLWARNING | NOT FOUND | SQLEXCEPTION } 游标应用中至少需要定义一个NOT FOUND的HANDLER(处理例程): DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1; 其含义是当抛出NOT FOUND异常时,置变量finished的值为1,程序继续运行。当然,在此之前,应当先定义变量finished,并初始化为0(也可在循环语句之前初始化为0),finished作为循环的控制变量,仅当finished变成1时,循环结束。 如果特情处理例程由多条语句组成,可以用BEGIN…END组成复合语句。 当一个存储过程中存在多个游标时,对任何一个游标的读取(FETCH)都可能会触发特情处理。比如一个游标的数据被遍历完毕,再试图FETCH下一行时,会触发NOT FOUND HANDLER, 并进而改变某个变量的值,但另一个游标中可能还有未处理完的数据。编程者应当自己想办法区分是哪个游标的数据处理完毕。 2 OPEN语句 OPEN cursor_name 该语句打开之前定义的游标,并初始化指向数据行的指针(接下来的第一条FETCH语句将试图读取游标的第1行数据)。 3 FETCH语句 FETCH [[NEXT] FROM] cursor_name INTO var_name [, var_name] ... ETCH语句读取游标的一行数据到变量列表,并推进游标的指针.关键词NEXT, FROM都可省略(或仅省略NEXT)。注意INTO后的变量列表应当与游标定义中的SELECT列表一一对应(变量个数与SELECT列表个数完全相同,数据类型完全一致,每个变量的取值按SELECT列表顺序一一对应)。 FETCH一个未打开的游标会出错。 4 CLOSE语句 CLOSE cursor_name Close语句关闭先前打开的游标,试图关闭一个未曾打开(OPEN)的游标会出错。 没有CLOSE的游标,在其定义的BEGIN…END语句块结束时,将自动CLOSE。 使用游标编写存储过程sp_cursor_demo计算Liverpool足球队在主场获胜的比赛中,上半场的平均进球数,结果通过参数传递。示例程序如下: DELIMITER $$ CREATE PROCEDURE sp_cursor_demo(INOUT average_goals FLOAT) BEGIN DECLARE done INT DEFAULT FALSE; DECLARE matches int DEFAULT(0); DECLARE goals int DEFAULT(0); DECLARE half_time_goals INT; DECLARE team_cursor CURSOR FOR SELECT HTHG FROM epl WHERE (home_team = 'Liverpool') and (ftr = 'H'); DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; OPEN team_cursor; FETCH team_cursor INTO half_time_goals; WHILE NOT DONE DO SET goals = goals + half_time_goals; SET matches = matches + 1; FETCH team_cursor INTO half_time_goals; END while; SET average_goals = goals / matches; CLOSE team_cursor; END $$ DELIMITER; 存储过程定义后,可通过以下语句定义参数,调用过程,再从返回参数中获取结果: SET @average_goals = 0.0; CALL sp_cursor_demo(@average_goals); SELECT @average_goals; 上述带前缀@的变量属于MySQL的用户自定义变量,只在该用户的会话期内有效,对别的用户(客户端)不可见。@前缀变量不用申明变量类型,初始化时,由其值决定其类型。 一般来说,仅当你需要遍历一个数据集,且一次只能处理其中的一行数据时(比如对每一行,要作不同的业务处理),你才需要使用游标。当游标的数据集较大时,会造成较大的网络时延。使用游标时,应尽可能缩小数据规模(去掉不必要的行和列)。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:2:1","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"3.3 使用事务的存储过程 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:3:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"事务的定义和应用 开启事务: START TRANSACTION 或 BEGIN (前者兼容性更好) 事务提交: COMMIT 事务回滚: ROLLBACK 开启或关闭当前会话的自动事务模式: SET autocommit = ON|OFF 也可用1|0,true|false代替ON|OFF。 缺省情况下,autocommit模式被设置为ON,即你在命令行提交的每一条语句会自动封装成一个事务,即使下一条语句发生错误,前一条语句产生的结果也不可撤销。 注意,事务内部不允许嵌套另一个事务,尽量不要在事务内部使用DDL语句,因为即使事务回滚,DDL语句对数据库的修改也不会撤销。 4 触发器 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:3:1","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"4.1 触发器 触发器是与某个表绑定的命名存储对象,与存储过程一样,它由一组语句组成,当这个表上发生某个操作(insert,delete,update)时,触发器被触发执行。触发器一般用于实现业务完整性规则。当primary key,foreigh key, check等约束都无法实现某个复杂的业务规则时,可以考虑用触发器来实现。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:4:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"4.2 触发器的创建 创建触发器的语句: CREATE TRIGGER trigger_name trigger_time trigger_event ON tbl_name FOR EACH ROW trigger_body trigger_nme: 每个触发器有一个唯一的命名 trigger_time: 触发的时机,二选一: BEFORE | AFTER trigger_event: 触发事件,三选一: INSERT | UPDATE | DELETE tbl_name: 与触发器绑定的表 trigger_body: 触发器程序体,可由变量定义、赋值,流程控制,SQL语句等组成。但触发器体内不能使用create,alter,drop等DDL语句,也不能使用start transaction, commit,rollback等事务相关语句。 与创建存储过程、函数一样,创建触发器时也要用delimiter语句重新指定触发器定义语句的界符(触发器内语句的分隔符仍为分号),在触发器定义之后,再把界符更改回去。 before与after触发器的区别: before触发器在试图激活触发器的那条语句(insert|delete|update)之前执行。 after触发器仅在before触发器(如果有的话)和试图激活触发器的那条语句都成功执行后才执行。 before触发器或after触发器如果未能成功执行,则激活触发器的语句也不会执行。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:5:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"4.3 触发器内的特殊表 在触发器内可以使用两类特殊表: old表和new表。它总是与触发器绑定的表有相同的结构,且只能在触发器内访问。 delete触发器可以访问old表,其内容为被delete掉的数据。 insert触发器可以访问new表,其内容为insert的新数据。 update触发器可以访问old表和new表,old表保存着修改前的数据,new表保存着修改后的内容。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:6:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"1 数据库、表与完整性约束的定义 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:0:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"1.1 创建数据库 进入mysql: mysql -h127.0.0.1 -uroot -p 创建数据库: CREATE DATABASE dbname; 指明访问的数据库: use dbname; ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:1:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"1.2 创建表及表的主码约束 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:2:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"建表语法 CREATE TABLE为保留字,其语义为创建表对象; IF NOT EXISTS为可选短语,其语义为仅当该表不存在时才创建表;如果不带该短语,创建表时,如果同名表已存在,则输出报错信息; tbl_name为表的名字; (列定义|表约束,…)表示表的其它定义都写在一对括号里,括号里为一个或多个“列定义”或“表约束”,如果有多个列的定义或表约束,则它们之间用逗号隔开。 CREATE TABLE [IF NOT EXISTS] tbl_name (列定义|表约束,...) ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:2:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"列定义语法 [NOT NULL |NULL]表示空或非空约束,缺省为NULL,即该列的内容允许为空值,NOT NULL则约束该列的内容必须为非空; DEFAULT关键字为列指定缺省值,可以是常量,也可以是表达式; AUTO_INCREMENT指定该列为自增列(如1,2,3,…),一般用于自动编号,显然只有数字类型的列才可以定义这一特性; [UNIQUE]指定该列值具有唯一性(但可以有空值-甚至多个空值的存在,如果该列没有定义NOT NULL约束); PRIMARY KEY指定该列为主码,相当于定义表的实体完整性约束;只有当主码由单属性组成时,才可以这样定义主码(主码由多属性组成时,应当用表约束来定义); COMMENT用来给列附加一条注释; “REFERENCES”短语为该列定义参照完整性约束,指出该列引用哪个表的哪一列的值,以及违背参照完整性后的具体处理规则(多个规则中选一),具体内容将在随后的练习里再讲解; CHECK(表达式)为列指定“自定义约束”,只有使(表达式)的值为true的数据才允许写入数据库;关键词CONSTRAINT用来为约束命名。 列定义 ::= 列名 数据类型 [NOT NULL | NULL] [DEFAULT {常量 | (表达式)} ] [AUTO_INCREMENT] [UNIQUE [KEY]] [PRIMARY KEY] [COMMENT '列备注'] [REFERENCES tbl_name (col_name) [ON DELETE RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT] [ON UPDATE RESTRICT|CASCADE|SET NULL| NO ACTION|SET DEFAULT]] [[CONSTRAINT [约束名]] CHECK (表达式)] ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:2:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"表约束语法 主码约束以“PK_”打头,后跟表名,一个表只会有一个主码约束; 外码约束以“FK_”打头,后跟表名及列名; CHECK约束以“CK_”打头,后跟表名及列名。 表约束 ::= [CONSTRAINT [约束名]] | PRIMARY KEY (key_part,...) | UNIQUE (key_part,...) | FOREIGN KEY (col_name,...) REFERENCES tbl_name (col_name,...) [ON DELETE RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT] [ON UPDATE RESTRICT|CASCADE|SET NULL| NO ACTION|SET DEFAULT] | CHECK (表达式) 主码约束及唯一性约束中“key_part”的语法规则如下: key_part::= {列名| (表达式)} [ASC | DESC] ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:2:3","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"定义主码 单属性主码,既可在列定义里用PRIMARY KEY约束指定主码,也可以作为表约束单独定义; 组合属性作主码时,该主码只能定义为表约束。 表创建好之后可以使用如下语句列出所有的表: show tables; 还可以使用如下语句查看表的结构,用来检查所建的表是否正确体现了原意: DESC 表名; ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:2:4","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"1.3 创建外码约束 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:3:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"外码 外码是表中的一个或一组字段(属性),它可以不是本表的主码,但它与某个主码(同一表或其它表的主码)具有对应关系(语义完全相同)。外码可以是一列或多列,一个表可以有一个或多个外码。当我们谈论外码时,一定有个主码与它对应,外码不可能单独存在。主码所在的表为主表,又称父表,外码所在的表为从表,又称子表。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:3:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"外码约束 外码用来在数据之间(即外码与其对应的主码间)建立关联。参照完整性约束用于约束外码列的取值范围:外码列的取值要么为空,要么等于其对应的主码列的某个取值。在语义允许,又不违反其它约束规则的情形下,外码列的取值才可以为空。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:3:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"定义外码约束 可在定义表的同时定义各种完整性约束规则(当然包括外码约束,亦即参照完整性约束)。外码约束既可以定义为列约束,亦可定义为表约束。 列级外码约束的语法格式如下: 列级外码约束 ::= 列名 数据类型 [REFERENCES tbl_name (col_name) [ON DELETE RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT] [ON UPDATE RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT] 表约外码约束的语法格式如下: 表级外码约束 ::= [CONSTRAINT [约束名]] FOREIGN KEY (col_name,...) REFERENCES tbl_name (col_name,...) [ON DELETE RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT] [ON UPDATE RESTRICT|CASCADE|SET NULL| NO ACTION|SET DEFAULT] MySQL表级外码约束的好处是可以给约束命名,且支持多属性组合外码(即外码由多个列组成)。**事实上,外码约束定义在表一级,是不二的选择,因为MySQL对列级外码约束的支持仅停留在语法检查阶段,实际并没有实现(至少8.0.22还没有实现)。**外码约束的名称一般以“FK_”为前缀,这是约定俗成的规则。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:3:3","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"1.4 check约束 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:4:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"用户定义的完整性约束 关系数据库的完整性约束共有三类:实体完整性约束,参照完整性约束以及用户定义的完整性约束。实体完整性约束和参照完整性约束分别用PRIMARY KEY和FOREIGN KEY来实现;CHECK约束是最主要的一类用户定义的完整性约束,用于定义用户对表中的某列的数据约束,或表中一行中几列之间应该满足的完整性约束。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:4:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"CHECK约束的定义方法 如果约束仅涉及单个列,则该约束既可以定义为列约束,也可以定义为表约束,例如:“性别”列的取值仅限从(“男”,“女”)中取值; 如果约束涉及表的多个列,则该约束只能定义为表约束,例如:如果职称为“教授”,则它的薪资应当不低于6000元。这个约束涉及到“职称”和“薪资”两个列的内容,故只能用表约束来实现。 CHECK约束的语法: CHECK约束 ::= [CONSTRAINT [约束名]] CHECK (条件表达式)] 只有当条件表达式的值为true时,数据(插入的新数据,或修改后的数据)才会被接受,否则将被拒绝。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:4:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"1.5 DEFAULT约束 默认值约束(Default约束)用于给表中的字段指定默认值,即往表里插入一条新记录时,如果没有给这个字段赋值,那么DBMS就会自动赋于这个字段默认值。 Default约束只能定义为列一级约束,即在需要指定默认值的列之后用关键字DEFAULT申明默认值,其语法为: col_name data_type [DEFAULT {literal | (expr)} ] 即在列名与列的数据类型之后申明Default约束。当然Default约束只是众多列约束中的一种,该列可能还有NOT NULL, UNIQUE, AUTO_INCREMENT, CHECK,FOREIGN KEY等其它约束。 DEFAULT关键字引出的默认值可以是常量,也可以是一个表达式。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:5:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"举例 AUTO_INCREMENT约束仅用于整数列; DEFAULT约束指定默认值为表达式时,表达式要写在一对括弧里; 这里,curdate()是MySQL的系统函数,其功能是取当前日期; 语句中,表名称order前后的符号是必须的,因为order是MySQL的关键字,当表名或列名与关键字冲突时,名称前后必须加`号。 create table `order`( orderNo int auto_increment primary key, orderDate date default (curdate()), customerNo char(10), employeeNo char(10)); ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:5:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"1.6 UNIQUE约束 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:6:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"UNIQUE约束 跟主码(Primary Key)约束一样,Unique约束既可以是对单属性的约束,也可以是对属性组约束,具有Unique约束的属性或属性组的取值必须是唯一的,否则就违反了Unique约束。不过,跟主码不同的是,Unique约束并不要求字段必须非空(Not Null),所以,实际上,它只能约束非空的属性(组)取值是唯一的。同时具有Not Null约束的Unique属性(组)相当于候选码。一个表只能定义一个主码约束,但可以定义多个Unique约束。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:6:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"UNIQUE约束的语法 跟主码约束一样,单字段的Unique约束既可定义为列约束,亦可定义为表约束,组合字段的Unique约束只能定义为表约束。 Unique列约束的语法为: col_name data_type UNIQUE Unique表约束的语法为: [CONSTRAINT [约束名]] UNIQUE(列1, 列2, ...) Constraint短语可以省略。既使写上关键词constraint,也可以省略约束名。约束未命名时,MySQL将按一定规则自动予以命名。 NOT NULL只能作列约束,且不用命名。UNIQUE约束作列约束时不能自主命名,作表约束时可以自主命名。 2 表结构与完整性约束的修改 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:6:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"2.1 修改表名 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:7:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"ALTER TABLE语句 Alter Table语句用于修改由Create Table语句创建的表的结构。比如,添加或删除列,添加或删除约束,创建或销毁索引,更改列的数据类型,更改列名甚至表名等。 ALTER TABLE 表名 [修改事项 [, 修改事项] ...] 常用修改事项有: 用ADD关键词添加列和约束(主码、外码、CHECK、UNIQUE等约束); 用DROP关键词删除列、约束和索引(含Unique); 用MODIFY关键词修改列的定义(数据类型和约束); 用RENAME关键词修改列、索引和表的名称; 用CHANGE关键词修改列的名称,同时还可以修改其定义(类型和约束)。 修改事项 ::= ADD [COLUMN] 列名 数据类型 [列约束] [FIRST | AFTER col_name] | ADD {INDEX|KEY} [索引名] [类型] (列1,...) | ADD [CONSTRAINT [约束名]] 主码约束 | ADD [CONSTRAINT [约束名]] UNIQUE约束 | ADD [CONSTRAINT [约束名]] 外码约束 | ADD [CONSTRAINT [约束名]] CHECK约束 | DROP {CHECK|CONSTRAINT} 约束名 | ALTER [COLUMN] 列名 {SET DEFAULT {常量 | (表达式)} | DROP DEFAULT} | CHANGE [COLUMN] 列名 新列名 数据类型 [列约束] [FIRST | AFTER col_name] | DROP [COLUMN] 列名 | DROP {INDEX|KEY} 索引名 | DROP PRIMARY KEY | DROP FOREIGN KEY fk_symbol | MODIFY [COLUMN] 列名 数据类型 [列约束] [FIRST | AFTER col_name] | RENAME COLUMN 列名 TO 新列名 | RENAME {INDEX|KEY} 索引名 TO 新索引名 | RENAME [TO|AS] 新表名 说明: 注意RENAME,MODIFY和CHANGE的区别:仅改列名,用RENAME; 只改数据类型不改名,用MODIFY; 既改名又改数据类型,用CHANGE。 在用MODIFY,CHANGE更改列的数据类型和约束时,修改后的CHECK约束并不会生效(MySQL只作语法检查,并未实现代码–至少MySQL 8.0.22还未实现)。但用ADD新增列的CHECK约束,是有效的。 删除主码约束只能用Drop Primary Key短语,不能使用drop constraint短语,即便在创建主码约束时显式命名了该主码约束。试图使用“drop constraint 主码约束名”短语删除主码,会给出错误提示,显示该约束并不存在。因为MySQL并没有完全实现“constraint 约束名 primary key(…)”短语的功能,仅作了语法检查,然后直接忽略了主码约束的命名。 给已有列增加Default约束,可用 alter 列 set default ... 短语;删除列的default约束,可用 alter 列 drop default 短语。当然,也可以用 Modify 列名 数据类型 ... 短语。如果该短语没有default约束,就相当于删除了原来的default约束,如果该短语带有default约束,就相当于添加了default约束,如果之前已有default约束,则新的Default约束将代替原有的Default约束; 删除unique约束,既可用 drop constraint 约束名 短语,也可以用 drop key 索引名 短语来实现,唯一性(unique)约束实际是用Unique索引来实现的,Unique索引的名字总是与Unique约束名完全一样,它们本就是同一套机制。如果没有显式命名的话,Unqiue索引名或者说Unique约束名一般与列同名(组合属性作索引,则与组合属性中的第1列同名)。但要注意是的,在更改列名后,Unique索引名并不会随之更改。在创建Unqiue约束时,用“constriant”短语给约束取一个有意义的名字,是一个值得推荐的习惯。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:7:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"更改表名 alter table 表名 rename [TO|AS] 新表名 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:7:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"2.2 添加与删除字段 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:8:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"给表添加字段 关键字FIRST指示新添加的列为第1列; AFTER指示新添加的列紧跟在指定列的后面。 如果省略位置指示,则新添加的列将成为表的最后一列。 关键字column可以省略。 ALTER TABLE 表名 ADD [COLUMN] 列名 数据类型 [列约束] [FIRST | AFTER 列名] 举个例子: alter table student add mobile char(11) constraint CK_student_mobile check(mobile rlike '1[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'); check约束中的rlike还可以用regexp替代,它们是同义语。跟Oracle一样,MySQL用正则匹配表达式来测试字段值是否符合某个pattern,rlike比like关键词所支持的功能要强大得多。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:8:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"删除表中的字段 关键字COLUMN可以省略,其语法格式为: ALTER TABLE 表名 DROP [COLUMN] 列名 举个例子: 在学生档案里记录年龄的作法并不科学,因为年龄会随着时间的变化而变化,档案里记录17岁,还得根据当年记录的日期以及当下的日期推算实际年龄。替代方案是记录出生日期而不是年龄。解决方案: # 第1步:添加列DOB alter table student add DOB date after sex; # 第2步,根据age推算DOB update student set DOB = date_add('2020-9-1', interval -1*age year); # date_add()是mysql的函数 select * from student; # 查看表student的内容 # 第3步,删除列age alter table student drop age; ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:8:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"2.3 修改字段 修改列名、列数据类型和列约束,以及列序的修改事项有: 修改事项 ::= ALTER [COLUMN] 列名 {SET DEFAULT {常量 | (表达式)} | DROP DEFAULT} | CHANGE [COLUMN] 列名 新列名 数据类型 [列约束] [FIRST | AFTER col_name] | MODIFY [COLUMN] 列名 数据类型 [列约束] [FIRST | AFTER col_name] | RENAME COLUMN 列名 TO 新列名 CHANGE短语可修改列名、数据类型和列约束; MODIFY短语可修改列的数据类型和约束; RENAME短语仅用于更改列名; ALTER短语仅用于修改列的DEFAULT约束或删除列的DEFAULT约束。 CHANGE和MODIFY短语还可以修改列在表中的位置。 create database MyDb; use MyDb; create table s( sno char(10) primary key, name varchar(32) not null, ID char(18) unique ); ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:9:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"修改字段名称 注意:关键字COLUMN不能省略 ALTER TABLE 表名 RENAME COLUMN 列名 TO 新列名 如果修改列名的同时,还要修改列的类型和约束,则用CHANGE短语: ALTER TABLE 表名 CHANGE [COLUMN] 列名 新列名 数据类型 [列约束] [FIRST | AFTER col_name] 如果新列带有CHECK约束的话,MySQL只会对这个约束作语法检查,并不会去实现这个约束,其它类型的约束没有问题。如果真有这样的需求,不如先DROP之前的列,再ADD新的列,新列附带的CHECK约束是会被实现的。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:9:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"修改字段的数据类型和约束 如果列名称不变,仅需要修改其数据类型和约束,则用MODIFY短语: ALTER TABLE 表名 MODIFY [COLUMN] 列名 数据类型 [列约束] [FIRST | AFTER col_name] 注意,一旦使用MODIFY短语修改列,则该列之前的数据类型、约束将被新的数据类型和约束取而代之。如果之前定义了列约束,修改后不带列约束,相当于删除了之前的约束。 如果需要修改(或添加)列的DEFAULT约束,则既可用上面的MODIFY短语,也可以使用ALTER短语: ALTER TABLE 表名 ALTER [COLUMN] 列名 SET DEFAULT {常量 | (表达式)} 删除列的DEFAULT约束,则可以使用ALTER短语(或MODIFY短语): ALTER TABLE 表名 ALTER [COLUMN] 列名 DROP DEFAULT ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:9:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"修改字段在表中的位置 如果仅需修改列在表中的位置,仍用MODIFY短语: ALTER TABLE 表名 MODIFY [COLUMN] 列名 数据类型 [列约束] [FIRST | AFTER col_name] 举个例子: alter table resident modify idNo char(18), modify height int unsigned, rename column educationalBackground to education; ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:9:3","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"2.4 添加、删除与修改约束 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:10:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"主码的添加与删除 删除主码: ALTER TABLE 表名 DROP PRIMARY KEY; drop index `PRIMARY` on 表名; 添加主码: ALTER TABLE 表名 ADD [CONSTRAINT [约束名]] PRIMARY KEY(列1,列2,...); MySQL尽管在语法上支持主码约束的命名,但实际上并没有真正实现主码约束的命名功能。即,MySQL并不会创建用户语句中所指定的约束名。所以,试图通过约束名删除主码约束是行不通的。 MySQL中,所有的主码约束(主码索引)名均为PRIMARY,无论怎么命名或更命,这个名字都不会改变。由于PRIMARY是MySQL的保留字,所以,在引用这个主码约束(索引)名时,必须用一对``符号将PRIMARY括起来。 举个例子: # 第1步:删除错误的主码定义 alter table score drop primary key; # 第2步:重新创建主码 alter table score add constraint PK_score primary key(sno,cno); alter table score add primary key(sno,cno); # 进阶版 alter table score drop primary key, add primary key(sno,cno); ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:10:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"外码的删除与添加 删除外码: ALTER TABLE 表名 DROP CONSTRAINT 约束名 ALTER TABLE 表名 DROP FOREIGN KEY 约束名 添加外码: ALTER TABLE 表名 ADD [CONSTRAINT [约束名]] 外码约束 约束名是可选的,如果省略命名短语,MySQL将按一定的规则自动命名。将来如果要删除该约束,必须先查询到该约束的名字(注:从MySQL的数据字典查询)。 创建外码时,MySQL将同步创建外码索引,如果外码约束有显式命名,则外码索引与外码约束同名。如果外码约束未命名,则外码索引与外码列的列名同名。 删除外码约束时,外码索引不会跟着删除。如果将来重新创建了外码,并显式命名,则外码索引会自动更名(与外码约束名保持相同)。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:10:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"CHECK约束的删除与添加 删除check约束: ALTER TABLE 表名 DROP CONSTRAINT 约束名 添加check约束: ALTER TABLE 表名 ADD [CONSTRAINT [约束名]] check(条件表达式) 添加约束时,如果现有数据与该约束规则相矛盾,则创建约束的请求会被拒绝。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:10:3","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"UNIQUE约束的添加与删除 删除Unique约束: alter table 表名 drop constraint 约束名; drop index 索引名 on 表名; 添加Unique约束: alter table 表名 ADD [CONSTRAINT [约束名]] UNIQUE(列1,...) 创建unique约束时,将同步创建unique索引,索引名与约束同名。如果未显式命名unique约束或索引,MySQL将按一定规则自动命名(单列的unique索引或约束与列同名)。 约束的修改一般通过先删除旧约束再重建新约束来实现。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:10:4","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["计算机网络"],"content":"最近在复习计算机网络考试,于是按照《计算机网络自顶向下方法》(原书第7版)一书梳理了1-7章的知识,其中第五章的内容合并到第四章中了。 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:0:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["计算机网络"],"content":"1 计算机网络和因特网 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:1:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["计算机网络"],"content":"2 应用层 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:2:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["计算机网络"],"content":"3 运输层 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:3:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["计算机网络"],"content":"4 网络层 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:4:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["计算机网络"],"content":"6 链路层和局域网 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:5:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["计算机网络"],"content":"7 无线网络和局域网 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:6:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["深度学习"],"content":"本文为论文 Label Embedding Online Hashing for Cross-Modal 的阅读笔记。 论文下载:https://doi.org/10.1145/3394171.3413971 ","date":"2022-11-18","objectID":"/lemon/:0:0","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"1 简介 学习散列,作为最著名的近似近邻搜索技术之一,近年来吸引了很多人的注意。它旨在将高维实值特征映射到紧凑的二进制编码,同时保留原始空间中的相似性。然后,可以用XOR操作在Hamming空间中进行搜索,效率高,存储成本低。 许多跨模态散列方法已经被提出并取得了很好的性能。但大多数现有的方法通过批处理学习二进制代码或哈希函数。即在学习过程前,所有的训练数据都可用。这将产生以下问题: 实际数据通常以流方式收集,有新数据到来时,批处理方法需要对所有数据重新训练 → 效率低 训练集随训练时间变大 → 计算成本高 为了解决这些问题,在线散列被提出,但仍存在问题: 大多数在线散列方法是为单模态检索设计的,很难直接扩展到跨模态检索。少数在线跨模态散列模型被提出,但性能较差,因为异质模态之间的关联性难以捕捉。 只根据新到达的数据更新散列函数,忽略了新旧数据间的相关性 → 丢失现有数据的信息 → 现有在线散列。 新数据到来时,哈希函数可以有效地重新训练,但哈希码必须对所有累积数据重构 → 更新方案低效。 离散优化大多采用松弛策略 → 量化误差大。 为了解决上述问题,这篇文章提出了一种新的监督下的跨模式检索的在线散列方法,即Label EMbedding ONline hashing,简称LEMON。本文的主要贡献总结如下: 提出了一种新的有监督的在线散列检索方法,即LEMON。 它通过一个标签嵌入框架来捕捉语义结构,其中包括标签相似性的保存和标签重构,从而得到更有辨识度的二进制码。 通过优化内积最小化问题将新旧数据的哈希码连接起来,解决了更新不平衡问题。 采用两步学习策略,有效地更新哈希函数和二进制码,同时保持旧数据库的哈希代码不可更改,使其计算复杂度仅与流数据的大小有关。 提出了一种迭代优化算法来离散地解决二进制优化问题,极大地减少 量化误差。 在三个基准数据集上的实验结果表明,LEMON在跨模式检索任务上优于一些先进的离线和在线散列方法,并且可以扩展到大规模数据集。 ","date":"2022-11-18","objectID":"/lemon/:1:0","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"2 相关工作 现有工作存在的问题: 单模态:不能直接用于跨模态检索任务;必须在每一轮更新所有的二进制代码,效率非常低 多模态:不能跨模态检索 跨模态:不能充分利用原始特征、语义标签;不能很好地以流的方式来捕捉数据的相似性信息 单模态:查询和要检索的文档都只有一个模态(图像→图像) 多模态:查询和要检索的文档必须至少有一个模态相同(图像、文本→图像、文本) 跨模态:查询和要检索的文档模态不同(图像→文本) ","date":"2022-11-18","objectID":"/lemon/:2:0","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"3 方法 ","date":"2022-11-18","objectID":"/lemon/:3:0","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"3.1 Notations 假设每个样本由 $l$ 个模态组成。在第 $t$ 轮,一个新的数据块 $\\vec{X}^{(t)}$ 被添加到数据库中。常用变量的说明如下: 符号 意义 $\\vec{X}_m^{(t)}∈R^{d_m×n_t}$ 表示新数据块的第 $m$ 个模态,其中 $n_t$ 是新数据块的大小, $d_m$ 是特征维度。$m∈{1,2,…,l}$ $\\vec{L}^{(t)}∈R^{c×n_t}$ 新数据块的标签矩阵,其中 $c$ 是语义类别的数量 $\\tilde{X}^{(t)}$ 现有数据 $N_{t-1} = \\sum_{k=1}^{t-1} n_k$ 现有数据的大小,$N_t=N_{t-1} +n_t$ $\\tilde{L}^{(t)}∈R^{c×N_{t-1}}$ 现有数据的相应标签矩阵 $X^{(t)}_m=[\\tilde{X}^{(t)}_m,\\vec{X}_m^{(t)}]∈R^{d_m×N_t}$ 代表当前整个数据库 $L^{(t)}=[\\tilde{L}^{(t)},\\vec{L}^{(t)}]∈R^{c×N_t}$ 代表整个标签矩阵 $\\tilde{B}^{(t)}$ 现有数据的哈希码 $\\vec{B}^{(t)}$ 新数据的哈希码 我们的目标是学习所有模态的 $r$ 位统一哈希码$B^{(t)}=[\\tilde{B}^{(t)},\\vec{B}^{(t)}]∈R^{r×N_t}$,和第$m$ 个模态的哈希函数 $H_m^{(t)}(·)$。 本文采用了一个两步学习方案:首先学习现有样本和新数据的哈希码,再基于学习到的哈希码,进一步学习哈希函数。 ","date":"2022-11-18","objectID":"/lemon/:3:1","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"3.2 Hash Codes Learning 算法整体伪代码: 3.2.1 Label Embedding 根据检索任务的目标,二进制代码应该保留实例的语义相似性。为了实现这一点,任务可以定义为以下的内积最小化问题: $$ min_{B(t)} ∥B^{(t)⊤}B^{(t)} − rS^{(t)}∥^2, s.t. B^{(t)} ∈ {−1, 1}^{r×N_t}\\tag{1} $$ $S_{(t)}$ 是语义相似度矩阵。如果第 $i$ 个实例和第 $j$ 个实例至少有一个共同的标签,则 $S^{(t)}_{ij} = 1$ ,否则 $S^{(t)}_{ij} = -1$ 。此方案存在的问题: 存储、计算成本大 不能表明细粒度的语义相似性,特别是对于多标签数据 为了解决上述问题,重新定义相似性矩阵,并通过二进制的哈希码保存。标签相似度矩阵如下: $$ S^{(t)} = 2U^{(t)⊤} U^{(t)} − 11^⊤\\tag{2} $$ 其中 $U^{(t)⊤}$ 是2规范化的标签矩阵,定义为 $u^{(t)}_i =l^{(t)}_i/∥l^{(t)}_i ∥$ ,而 $l^{(t)}_i$ 是 $L^{(t)}$ 的第 $i$ 列。 为了使 $S^{(t)}$ 能够用于在线场景,进一步将其改写为一个块状矩阵。$S_{oo}^{(t)}$,$S_{oc}^{(t)}$,$S_{co}^{(t)}$,$S_{cc}^{(t)}$分别是旧数据的成对相似度矩阵、旧新数据的相似度矩阵、旧新数据的相似度矩阵、新数据的成对相似性矩阵。 我们试图将更多的标签信息嵌入待学习的二进制码中。假设所有样本标签都可以从学习到的二进制码中重构。可以进一步定义以下优化问题: $$ min_{{B,P}^{(t)}} ∥L^{(t)} −P^{(t)}B^{(t)}∥^2+γ ∥P^{(t)}∥^2, s.t. B^{(t)} ∈ {−1, 1}^{r×N_t} \\tag{5} $$ 其中 $γ$ 是惩罚参数,$P^{(t)}∈R^{c×r}$ 是投影矩阵。合并(1)和(5): $$ min_{{B,P}^{(t)}} α ∥B^{(t)⊤}B^{(t)} − rS^{(t)}∥^2+ β∥L^{(t)} −P^{(t)}B^{(t)}∥^2+βγ ∥P^{(t)}∥^2 \\tag{6} $$ 其中 $α$ 和 $β$ 是权衡参数。显然,上述方法通过保留标签的相似性、重建标签,可以将更多的语义信息嵌入二进制码中。此外,它通过最一致的语义标签来匹配异质性的模式,从而产生统一的二进制码,非常适用于在线学习。 3.2.2 Online Learning 理想情况下,我们希望保持 $\\tilde{B}^{(t)}$ 不变,只更新 $\\vec{B}^{(t)}$ 。将(3)代入(6)得: $$ min_{{B,P}^{(t)}} α ∥\\vec{B}^{(t)⊤}\\tilde{B}^{(t)} − rS_{co}^{(t)}∥^2+ α ∥\\tilde{B}^{(t)⊤}\\vec{B}^{(t)} − rS_{oc}^{(t)}∥^2 + α ∥\\vec{B}^{(t)⊤}\\vec{B}^{(t)} − rS_{cc}^{(t)}∥^2 + $$ $$ β∥\\vec{L}^{(t)} −P^{(t)}\\vec{B}^{(t)}∥^2+ β∥\\tilde{L}^{(t)} −P^{(t)}\\tilde{B}^{(t)}∥^2+βγ ∥P^{(t)}∥^2, \\tag{7} $$ 这个在线目标是由最初的批处理目标推导出来的,即公式(6)。包含 $S_{oo}^{(t)}$ 的项被切断,因为 $\\tilde{B}^{(t)}$ 不变。因此,它对传入的数据不太敏感,能够产生更多的鉴别性哈希码。通过上述目标函数,新数据之间的成对相似性被保留。更重要的是,新旧数据之间的关联性也被 $S_{oc}^{(t)}$ 或 $S_{co}^{(t)}$ 捕获。因此,公式(7)能将更多的相似性信息嵌入到二进制码中。 然而随着时间的积累,旧数据库的样本数量比新数据块的大得多,导致相似性矩阵 $S_{co}^{(t)}$ 稀疏且不平衡,大多数元素是负的。使用直接的离散优化可能会带来巨大的信息损失,因为硬二进制矩阵分解可能会偏向于保持不相似的信息而丢失相似的信息。为了解决这个问题,我们用一个实值 $V^{(t)}$ 来代替一个 $B^{(t)}$。类似地,有 $V^{(t)}=[\\tilde{V}^{(t)},\\vec{V}^{(t)}]$ 。为了减少 $B^{(t)}$ 和 $V^{(t)}$ 间的信息损失,引入一个正交旋转矩阵的正则化项:$R^{(t)}∈R^{r×r}$ 。为了使 $V^{(t)}$ 无偏,引入正交和平衡约束。目标函数成为以下函数: $$ min_{{\\vec{B},\\vec{V},P,R}^{(t)}} ∥\\vec{B}^{(t)}−R^{(t)}\\vec{V}^{(t)}∥^2+∥\\tilde{B}^{(t)}−R^{(t)}\\tilde{V}^{(t)}∥^2 + α ∥\\vec{V}^{(t)⊤}\\tilde{B}^{(t)} − rS_{co}^{(t)}∥^2+ $$ $$ α ∥\\tilde{V}^{(t)⊤}\\vec{B}^{(t)} − rS_{oc}^{(t)}∥^2 + α ∥\\vec{V}^{(t)⊤}\\vec{B}^{(t)} − rS_{cc}^{(t)}∥^2 + β∥\\vec{L}^{(t)} −P^{(t)}\\vec{V}^{(t)}∥^2+ $$ $$ β∥\\tilde{L}^{(t)} −P^{(t)}\\tilde{V}^{(t)}∥^2+βγ ∥P^{(t)}∥^2, \\tag{8} $$ 这样一来,二元约束只强加在一个被分解的变量上,而且避免了二元矩阵分解。 此外,实值 $V^{(t)}$ 比 $B^{(t)}$ 能更准确地捕捉语义信息,确保在相似性保存过程中可接受的信息损失,从而解决更新不平衡问题。此外,它仍然保留了离散的约束条件,并通过sign(·)操作有效地生成二进制哈希码。$V^{(t)}$ 在相似性空间和标签空间之间架起了桥梁。 3.2.3 Efficient Discrete Optimization 为了解决公式 (8) 的问题,我们提出了四步迭代优化算法,该算法有效地、不连续地生成哈希码。在每个步骤中,一个变量被更新,而其他变量被固定。 更新 $P^{(t)}$ 。令公式 (8) 关于 $P^{(t)}$ 的导数为零,得出 $P^{(t)}$ : $$ P^{(t)} = C^{(t)}_1 (C^{(t)}_2 + γ I)^{−1} \\tag{9} $$ 更新 $\\vec{V}^{(t)}$ 。当Bfi(t)、P(t)、R(t)固定时,公式(8)可以被简化为: $$ max_{\\vec{V}^{(t)}} tr(Z\\vec{V}^{(t)}), s.t. \\vec{V}^{(t)}\\vec{V}^{(t)T} = n_tI, \\vec{V}^{(t)}1 = 0. \\tag{13} $$ 我们可以发现,方程(13)有一个封闭形式的最优解。记为 $J = I - \\frac{1}{nt} 11^⊤$。注意 $J$ 是实时计算的。然后,该问题可以通过对 ${ZJZ}^⊤$ 进行奇异值分解来解决: $$ {ZJZ}^⊤=\\left[\\begin{matrix}G \u0026 \\hat{G}\\end{matrix}\\right] \\left[\\begin{matrix}Ω \u0026 0 \\ 0 \u0026 0\\end{matrix}\\right] \\left[\\begin{matrix}G \u0026 \\hat{G}\\end{matrix}\\right]\\tag{16} $$ 这里,$Ω∈R^{r^′×r^′}$ 是正特征值的对角矩阵,$G∈R^{r×r^′}$ 是相应的特征向量。$\\hat{G}$ 是剩余的 $r - r^′$ 零特征值的特征向量。$r'$ 是 $ZJZ^⊤$ 的等级。通过对 $\\hat{G}$ 进行Gram-Schmidt处理,可以得到一个正交矩阵 $\\bar{G}∈R^{r×(r-r^′)}$。我们进一步表示 $Q = JZ^⊤GΩ^{-1/2}$,并生成一个随机的正交矩阵 $\\bar{Q}∈R^{n_t×(r-r^′)}$ 。如果 r′=r,$\\bar{G}$ 和 $\\bar{Q}$ 为空。则公式(13)的最优解是: $$ \\vec{V}^{(t)} = \\sqrt{n_t} \\left[\\begin{matrix}G \u0026 \\bar{G}\\end{matrix}\\right] \\left[\\begin{matrix}Q \u0026 \\bar{Q}\\end{matrix}\\right]^T.\\tag{17} $$ 更新 $\\vec{R}^{(t)}$ 。在除 $\\vec{R}^{(t)}$ 之外的所有变量固定的情况下,公式(8)变成经典的正交普鲁斯特问题,可以通过重度分解来解决: $$ C^{(t)}_5 = AΣ\\hat{A}^⊤,\\tag{18} $$ $$ C^{(t)}_5 = C^{(t-1)}_5+\\vec{B}^{(t)}\\vec{V}^{(t)T}, C^{(t-1)}_5=\\tilde{B}^{(t)}\\tilde{V}^{(t)T} .\\tag{19} $$ 得到 $R^{(t)}$ 的最佳解为: $$ R^{(t)} = A\\","date":"2022-11-18","objectID":"/lemon/:3:2","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"3.3 Hash Functions Learning 获得统一的二进制码后,需要学习哈希函数来适应多种模式。为了达到这个目的,可以采用不同的模型,如线性回归、支持向量机、深度神经网络。我们在学到的二进制码的监督下,为每种模式训练一个线性回归模型。具体来说,给定学习的二进制码 $B^{(t)}$ 和第 $m$ 个模态的特征矩阵 $X^{(t)}_m$,线性映射模型可以通过解决以下问题被学习: $$ min_{W^{(t)}_m} ∥B^{(t)} − W^{(t)}_m X^{(t)}_m∥^2 + ξ ∥W^{(t)}_m∥^2,\\tag{24} $$ 其中 $ξ$ 是一个惩罚参数,$W^{(t)}_m$ 是映射矩阵。通过将公式 (25) 关于 $W^{(t)}_m$ 的导数设为零,得到最佳解: $$ W^{(t)}_m = H^{(t)}_m(F^{(t)}_m + ξ I)^{−1},\\tag{26} $$ $$ H^{(t)}_m = H^{(t-1)}_m+\\vec{B}^{(t)}\\vec{X}^{(t)T}_m, H^{(t-1)}_m=\\tilde{B}^{(t)}\\tilde{X}^{(t)T}_m, $$ $$ F^{(t)}_m = F^{(t-1)}_m+\\vec{X}^{(t)}\\vec{X}^{(t)T}_m, F^{(t-1)}_m=\\tilde{X}^{(t)}\\tilde{X}^{(t)T}_m .\\tag{27} $$ 此后,给定一个新的查询,我们可以取第 $m$ 个模态 $x_m∈R^{d_m}$,并通过以下哈希函数生成其哈希码: $$ H^{(t)}_m (x_m) = sign(W^{(t)}_mx_m).\\tag{28} $$ 训练过程的整体计算复杂度与新数据的大小 $n_t$ 呈线性关系,而与旧数据库的大小无关。因此,LEMON可以扩展到大规模的在线检索任务。 ","date":"2022-11-18","objectID":"/lemon/:3:3","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"4 实验 为了评估LEMON的性能,我们在三个基准数据集上进行了广泛实验。 ","date":"2022-11-18","objectID":"/lemon/:4:0","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"4.1 实施细节 在实现LEMON的过程中,我们首先对MIRFlickr-25K数据集进行了参数敏感性分析,当α和β为 1e4 时,LEMON取得最佳效果。此外我们观察到,参数对性能的影响并不显著。因此,为了简单起见,所有的数据集上都设置了相同的参数,即α=β=1e4。根据经验,$γ$ 和 $ξ$ 分别被设定为0.1和1。每一轮的迭代次数为5。 在实验中,我们进行了两种类型的跨模式检索任务。图像→文本和文本→图像。利用平均精度(MAP)和训练时间,来评估所有方法的性能。 ","date":"2022-11-18","objectID":"/lemon/:4:1","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"4.2 结果分析 4.2.1 平均精度分析 MIRFlickr-25K数据集的结果如表1所示,展示了不同长度样例的MAP值。 不同方法的性能如下图所示: 可以得出: LEMON在所有情况下都持续优于其他方法,表明其在处理跨模式检索任务方面的有效性。 一般来说,离线基线(如DLFH和SCRATCH),比在线基线(如OCMH和OLSH)表现更好。 从图1中可以看到,LEMON实现了持续的性能提高,证明了LEMON可以通过新旧数据库之间的相关性,将更多的语义信息嵌入二进制码。 随着轮次的增加,大多数离线方法的性能都在下降。最有可能的原因是它们在每轮中用所有累积的样本重新训练哈希函数和哈希代码。 大多数方法随着哈希码长度的增加而表现得更好,表明更长的比特可以携带更多的鉴别信息。 大多数方法在文本→图像的任务中比图像→文本的任务中表现得更好,可能的原因是,文本特征可以更好地描述。 其他两个数据集的结果与其类似。 4.2.2 时间成本分析 根据之前的分析知,LEMON的复杂性与新数据的大小呈线性关系。由图四可知,LEMON在所有情况下都是最快的。离线方法需要更长的训练时间,并且时间成本随着回合数的增加而显著增加,因为它们必须对所有累积的数据重新训练哈希函数,这使得它们在在线情况下效率很低;而在线方法的训练时间不会。因此LEMON的训练非常有效,并且可以扩展到大规模在线检索。 4.2.3 参数敏感度分析 我们进行了实验来分析包括α和β在内的参数的敏感性。由图5可以看到,这些参数确实对LEMON的性能有一些影响,但并不明显。 ","date":"2022-11-18","objectID":"/lemon/:4:2","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["Git"],"content":"问题描述 将项目文件push到GitHub上时,发现GitHub上的文件夹图标上有箭头,且无法打开。 ","date":"2022-11-06","objectID":"/git%E6%8A%A5%E9%94%99-github%E6%96%87%E4%BB%B6%E5%A4%B9%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E4%B8%94%E6%97%A0%E6%B3%95%E6%89%93%E5%BC%80/:1:0","tags":["Github","Git"],"title":"Git报错-GitHub文件夹出现箭头且无法打开","uri":"/git%E6%8A%A5%E9%94%99-github%E6%96%87%E4%BB%B6%E5%A4%B9%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E4%B8%94%E6%97%A0%E6%B3%95%E6%89%93%E5%BC%80/"},{"categories":["Git"],"content":"出错原因 当在自己的项目里clone了别人的项目,github就将他视为一个子系统模块,导致在上传代码时该文件夹上传失败,并在github上显示向右的白色箭头。 ","date":"2022-11-06","objectID":"/git%E6%8A%A5%E9%94%99-github%E6%96%87%E4%BB%B6%E5%A4%B9%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E4%B8%94%E6%97%A0%E6%B3%95%E6%89%93%E5%BC%80/:2:0","tags":["Github","Git"],"title":"Git报错-GitHub文件夹出现箭头且无法打开","uri":"/git%E6%8A%A5%E9%94%99-github%E6%96%87%E4%BB%B6%E5%A4%B9%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E4%B8%94%E6%97%A0%E6%B3%95%E6%89%93%E5%BC%80/"},{"categories":["Git"],"content":"解决方案 删除子文件夹里面的.git文件,执行如下命令: git rm --cached [文件夹名] git add [文件夹名] git commit -m \"commit messge\" git push origin main ","date":"2022-11-06","objectID":"/git%E6%8A%A5%E9%94%99-github%E6%96%87%E4%BB%B6%E5%A4%B9%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E4%B8%94%E6%97%A0%E6%B3%95%E6%89%93%E5%BC%80/:3:0","tags":["Github","Git"],"title":"Git报错-GitHub文件夹出现箭头且无法打开","uri":"/git%E6%8A%A5%E9%94%99-github%E6%96%87%E4%BB%B6%E5%A4%B9%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E4%B8%94%E6%97%A0%E6%B3%95%E6%89%93%E5%BC%80/"},{"categories":["Git"],"content":"问题描述 在将本地的远程仓库push到github上时,出现报错: ssh: Could not resolve hostname github.com: Temporary failure in name resolution fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. ","date":"2022-11-06","objectID":"/git%E6%8A%A5%E9%94%99-ssh%E7%9B%B8%E5%85%B3%E9%94%99%E8%AF%AF/:1:0","tags":["SSH","Github","Git","Ping"],"title":"Git报错-ssh相关错误","uri":"/git%E6%8A%A5%E9%94%99-ssh%E7%9B%B8%E5%85%B3%E9%94%99%E8%AF%AF/"},{"categories":["Git"],"content":"情况1 ssh错误 解决方案:重新设置ssh 1 重新在git设置身份的名字和邮箱 进入到需要提交的文件夹底下,执行命令: git config --global user.name \"yourname\" git config --global user.email \"your@email.com\" 注:yourname是你要设置的名字,your@email是你要设置的邮箱。 2 删除known_hosts文件 进入 .ssh 文件夹,手动删除 known_hosts 文件 3 重新设置ssh git输入命令: ssh-keygen -t rsa -C \"your@email.com\" 接着一路回车,系统会自动在 .ssh 文件夹下生成两个文件,id_rsa和id_rsa.pub,用记事本打开 id_rsa.pub,复制里面的全部内容。 4 在github上新建SSH key 进入GitHub网站的个人设置界面,在 SSH and GPG keys 中新建一个SSH key,将刚刚复制的密钥粘贴进去。 5 验证是否添加成功 在git中输入命令: ssh -T git@github.com 接着跳出一段话,输入命令:yes,提示重新设置成功! ","date":"2022-11-06","objectID":"/git%E6%8A%A5%E9%94%99-ssh%E7%9B%B8%E5%85%B3%E9%94%99%E8%AF%AF/:2:0","tags":["SSH","Github","Git","Ping"],"title":"Git报错-ssh相关错误","uri":"/git%E6%8A%A5%E9%94%99-ssh%E7%9B%B8%E5%85%B3%E9%94%99%E8%AF%AF/"},{"categories":["Git"],"content":"情况2 DNS错误 排除了ssh的问题后,在cmd中对目标地址进行ping操作: ping github.com 出现如下错误提示: Ping request could not find host github.com. Please check the name and try again. 说明DNS出现网络问题,解决方案如下: 1 首先获取 github.com IP 地址 IP 地址查询: Click 通过上述网站查询得到 github.com IP 地址如下 140.82.113.4 2 修改hosts文件 以管理员身份打开本地 C:\\Windows\\System32\\drivers\\etc 目录下的 hosts 文件,在文件最下方添加: 140.82.113.4 github.com 完成后保存即可。 3 再次ping github.com 此时再次ping github.com即可看到能够成功ping通。 $ ping 140.82.113.4 Pinging 140.82.113.4 with 32 bytes of data: Reply from 140.82.113.4: bytes=32 time=229ms TTL=42 Reply from 140.82.113.4: bytes=32 time=229ms TTL=42 Reply from 140.82.113.4: bytes=32 time=229ms TTL=42 Reply from 140.82.113.4: bytes=32 time=229ms TTL=42 Ping statistics for 140.82.113.4: Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), Approximate round trip times in milli-seconds: Minimum = 229ms, Maximum = 229ms, Average = 229ms 再次进行git push操作,可以顺利执行。 ","date":"2022-11-06","objectID":"/git%E6%8A%A5%E9%94%99-ssh%E7%9B%B8%E5%85%B3%E9%94%99%E8%AF%AF/:3:0","tags":["SSH","Github","Git","Ping"],"title":"Git报错-ssh相关错误","uri":"/git%E6%8A%A5%E9%94%99-ssh%E7%9B%B8%E5%85%B3%E9%94%99%E8%AF%AF/"},{"categories":["Android"],"content":"1 安装软件 打开命令行,进入apk文件所在目录 输入命令:adb install xxx.apk 2 踩雷记录 ","date":"2022-11-06","objectID":"/%E4%BD%BF%E7%94%A8adb%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85%E8%BD%AF%E4%BB%B6/:0:0","tags":["adb","Android"],"title":"使用adb命令安装软件","uri":"/%E4%BD%BF%E7%94%A8adb%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85%E8%BD%AF%E4%BB%B6/"},{"categories":["Android"],"content":"报错1 android adb devices offline 解决办法:重启adb服务 adb kill-server adb start-server ","date":"2022-11-06","objectID":"/%E4%BD%BF%E7%94%A8adb%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85%E8%BD%AF%E4%BB%B6/:0:1","tags":["adb","Android"],"title":"使用adb命令安装软件","uri":"/%E4%BD%BF%E7%94%A8adb%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85%E8%BD%AF%E4%BB%B6/"},{"categories":["Android"],"content":"报错2 Failed to install app-debug.apk: Failure [INSTALL_FAILED_TEST_ONLY: installPackageLI] 解决办法:允许安装test用的apk adb install -t app-debug.apk ","date":"2022-11-06","objectID":"/%E4%BD%BF%E7%94%A8adb%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85%E8%BD%AF%E4%BB%B6/:0:2","tags":["adb","Android"],"title":"使用adb命令安装软件","uri":"/%E4%BD%BF%E7%94%A8adb%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85%E8%BD%AF%E4%BB%B6/"},{"categories":["Mysql"],"content":"9 数据库应用开发(JAVA篇) ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:0:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"9.1 JDBC体系结构和简单的查询 JDBC的体系结构 JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。 Java 具有坚固、安全、易于使用、易于理解和可从网络上自动下载等特性,是编写数据库应用程序的杰出语言。所需要的只是 Java应用程序与各种不同数据库之间进行对话的方法。 JDBC可以在各种平台上使用Java,如Windows,Mac OS和各种版本的UNIX。 JDBC API支持用于数据库访问的两层和三层处理模型,但通常,JDBC体系结构由两层组成: JDBC API:这提供了应用程序到JDBC管理器连接。 JDBC驱动程序API:这支持JDBC管理器到驱动程序连接。 JDBC的核心组件 JDBC的核心组件包括: DriverManager: 此类管理数据库驱动程序列表。使用通信子协议将来自java应用程序的连接请求与适当的数据库驱动程序匹配。 Driver:此接口处理与数据库服务器的通信,我们很少会直接与Driver对象进行交互。而是使用DriverManager对象来管理这种类型的对象。 Connection:该界面具有用于联系数据库的所有方法。连接对象表示通信上下文,即,与数据库的所有通信仅通过连接对象。 Statement:使用从此接口创建的对象将SQL语句提交到数据库。除了执行存储过程之外,一些派生接口还接受参数。 ResultSet:在使用Statement对象执行SQL查询后,这些对象保存从数据库检索的数据。它作为一个迭代器,允许我们遍历其数据。 SQLException:此类处理数据库应用程序中发生的任何错误 使用步骤 构建JDBC应用程序涉及以下六个步骤: 导入包:需要包含包含数据库编程所需的JDBC类的包。大多数情况下,使用import java.sql.*就足够了。 注册JDBC驱动程序:要求您初始化驱动程序,以便您可以打开与数据库的通信通道。 打开连接:需要使用DriverManager.getConnection()方法创建一个Connection对象,该对象表示与数据库的物理连接。 执行查询:需要使用类型为Statement的对象来构建和提交SQL语句到数据库。 从结果集中提取数据:需要使用相应的ResultSet.getXXX()方法从结果集中检索数据。 释放资源:需要明确地关闭所有数据库资源,而不依赖于JVM的垃圾收集。 建立JDBC连接所涉及的编程可简单概括为以下四个步骤 导入JDBC包:将Java语言的import语句添加到Java代码中导入所需的类。 注册JDBC驱动程序:此步骤将使JVM将所需的驱动程序实现加载到内存中,以便它可以满足您的JDBC请求。 数据库URL配置:这是为了创建一个格式正确的地址,指向要连接到的数据库。 创建连接对象:最后,调用DriverManager对象的getConnection()方法来建立实际的数据库连接。 Class.forName(); 注册驱动程序最常见的方法是使用Java的Class.forName()方法,将驱动程序的类文件动态加载到内存中,并将其自动注册。 try { Class.forName(\"com.mysql.cj.jdbc.Driver\"); }catch(ClassNotFoundException ex) { System.out.println(\"Error: unable to load driver class!\"); System.exit(1); } 数据库URL配置 加载驱动程序后,可以使用DriverManager.getConnection()方法建立连接。为了方便参考,特列出三个重载的DriverManager.getConnection()方法: getConnection(String url) getConnection(String url,Properties prop) getConnection(String url,String user,String password) 创建数据库连接对象 String URL = \"jdbc:mysql://localhost:3306/dbname?serverTimezone=UTC\"; String USER = \"username\"; String PASS = \"password\" Connection conn = DriverManager.getConnection(URL, USER, PASS); 完整的连接地址: jdbc:mysql://127.0.0.1:3306/dbname?useUnicode=true\u0026characterEncoding=UTF8\u0026useSSL=false\u0026serverTimezone=UTC\" 关闭数据库连接 为确保连接关闭,您可以在代码中提供一个“finally”块。一个finally块总是执行,不管是否发生异常。 要关闭上面打开的连接,你应该调用close()方法如下: conn.close(); JDBC执行SQL语句 一旦获得了连接,我们可以与数据库进行交互。JDBC Statement和PreparedStatement接口定义了能够发送SQL命令并从数据库接收数据的方法和属性。 创建Statement对象 在使用Statement对象执行SQL语句之前,需要使用Connection对象的createStatement()方法创建Statement的一个实例,如下例所示: Statement stmt = null; try { stmt = conn.createStatement( ); . . . } catch (SQLException e) { . . . } finally { . . . } 执行Statement对象 创建Statement对象后,您可以使用它来执行一个SQL语句,其中有三个执行方法之一。 boolean execute(String SQL):如果可以检索到ResultSet对象,则返回一个布尔值true; 否则返回false。使用此方法执行SQL DDL语句或需要使用真正的动态SQL时。 int executeUpdate(String SQL):返回受SQL语句执行影响的行数。使用此方法执行预期会影响多个行的SQL语句,例如INSERT,UPDATE或DELETE语句。 ResultSet executeQuery(String SQL):返回一个ResultSet对象。当您希望获得结果集时,请使用此方法,就像使用SELECT语句一样。 关闭Statement对象 就像关闭一个Connection对象以保存数据库资源一样,由于同样的原因,还应该关闭Statement对象。 调用close()方法即可关闭Statement对象。如果先关闭Connection对象,它也会关闭Statement对象。但是,应始终显式关闭Statement对象,以确保正确清理。 Statement stmt = null; try { stmt = conn.createStatement( ); . . . } catch (SQLException e) { . . . } finally { stmt.close(); } PreparedStatement PreparedStatement的接口扩展了Statement接口,其优点是可以动态地提供参数。如果语句被多次执行,其执行效率比Statement高。 PreparedStatement pstmt = null; try { String SQL = \"Update Employees SET age = ? WHERE id = ?\"; pstmt = conn.prepareStatement(SQL); . . . } catch (SQLException e) { . . . } finally { pstmt.close(); } JDBC中的所有参数都用?符号代替,这被称为参数标记(又叫占位符)。在执行SQL语句之前,必须为每个参数提供值。 用setXXX(参数序号,参数值)方法将值绑定到对应参数,其中XXX代表要绑定到输入参数的值的Java数据类型。如果忘记提供值,将收到一个SQLException。参数的序号从1开始。 记得调用close()方法,显示关闭PreparedStatement对象。 ResultSet ResultSet对象维护指向结果集中当前行的游标。有多种类型的“游标”,如果没有指定任何ResultSet类型,则取缺省值TYPE_FORWARD_ONLY。 类型 类型描述 ResultSet.TYPE_SCROLL_INSENSITIVE 光标可以向前和向后滚动,结果集对创建结果集后发生的数据库的其他更改不敏感。 ResultSet.TYPE_SCROLL_SENSITIVE 光标可以向前和向后滚动,结果集对创建结果集之后发生的其他数据库所做的更改敏感。 ResultSet.TYPE_FORWARD_ONLY 光标只能在结果集中向前移动。 ResultSet的遍历 用ResultSet的next()方法取得游标当前行的值,并用getXXX(列名)方","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:1:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"9.2 条件不确定的查询 JDBC的Statement类方法executeQuery(sql)可以执行一条确定的sql语句,如果要执行的sql语句带有变化的部分,比如每个客户的输入的用户名与密码都会不同. 这种情况,有两种解决方案: 把变量直接拼接到sql语句中 下例中,假定变量userName(类型String)在此前已被赋值: statement = connection.createStatement(); String sql = \"select * from user where username = '\" + userName + \"';\"; resultSet = statement.executeQuery(sql); 用PreparedStatement 用PreparedStatement类,把sql语句中变化的部分当成参数: String sql = \"select * from user where username = ? ;\"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1,userName); resultSet = preparedStatement.executeQuery(); ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:2:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"9.3 JDBC的插入操作 在成功连接数据库后,实例化Statement或PreparedStatement类的一个对象。该对象关联一条sql insert语句,然后调用对象的executeUpdate()方法即可。以PreparedStatement为例: String sql = \"insert into user values(?,?)\"; pps = connection.prepareStatement(sql); pps.setString(1,loginName); pps.setString(2,loginPass); int n = pps.executeUpdate(); if (n \u003e 0) { System.out.println(\"执行成功,影响行数:\" + n); } else { System.out.println(\"执行失败.\"); } 第2行出现的pps和connection分别为PreparedStatment和Connection类的一个对象(均在该代码段之前已实例化)。第3行和第4行出现的loginName和loginPass是两个String对象,假定此前已被赋值。 ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:3:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"9.4 事务处理 JDBC的事务处理 JDBC缺省情形下,Statement和PreapredStatement均自动为一个事务。因此,当一个Statement调了executeUpdate()方法后,所执行的SQL语句自动提交,其对数据库的修改不可再撤销。 但调用Connection.setAutoCommit(boolean autoCommit)方法可以改变缺省设置: connection.setAutoCommit(false); 将使本会话期内的语句不再自动提交,必须调用Connection的以下方法手动提交: commit() rollback() 前者为正常提交,后者表示事务回滚,撤销所有修改。 JDBC的事务隔离级别 JDBC通过调用Connetion的以下方法设置事务的隔离级别: void setTransactionIsolation(int level) 如果没有调用该方法设置隔离级别,将采用DBMS缺省的隔离级别,对MySQL而言,即repeatable read。JDBC支持以下隔离级别: static int TRANSACTION_NONE static int TRANSACTION_READ_UNCOMMITTED static int TRANSACTION_READ_COMMITTED static int TRANSACTION_REPEATABLE_READ static int TRANSACTION_SERIALIZABLE 它们对应的int值依次为0,1,2,4和8。 不是每个DBMS都支持以上所有的隔离级别。MySQL支持后面4个隔离级别,且缺省状态下为TRANSACTION_REPEATABLE_READ。Oracle支持其中的两个,缺省值为TRANSACTION_READ_COMMITTED。 10 数据库设计与实现 ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:4:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"10.1 从需求分析到逻辑模型 ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:5:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"业务功能描述 设计一个影院管理系统。影院对当前的放映厅和电影进行排片,顾客到来后,可以购买任一排场的电影票,进入对应放映厅观看。系统中有以下实体集: 电影(movie):属性有标识号(movie_ID)、电影名(title)、类型(type)、时长(runtime)、首映日期(release_date)、导演姓名(director)、主演姓名(starring)。 顾客(customer):属性有标识号(c_ID)、姓名(name)、手机号(phone)。 放映厅(hall):属性有标识号(hall_ID)、放映模式(mode)、容纳人数(capacity)、位置(location)。 排场(schedule):属性有标识号(schedule_ID)、日期(date)、时间(time)、票价(price)、票数(number)。 电影票(ticket):属性有标识号(ticket_ID)、座位号(seat_num)。 实体间的关系描述如下: ①. 顾客和电影票有一对多的购买关系。每位顾客可以买多张电影票,每张电影票被一位顾客购买。 ②. 电影票和排场有多对一的属于关系。一张电影票只属于一个排场,一个排场有多张电影票。 ③. 排场和电影有一对多的放映关系。每个排场放一部电影,每部电影可以在多个排场放映。 ④. 排场和放映厅有一对多的位于关系。每个排场位于一个放映厅,每个放映厅可以安排多个排场。 ER图: 关系模式: movie(movie_ID,title,type,runtime,release_date,director,starring),主码:(movie_ID) customer(c_ID,name,phone),主码:(c_ID) hall(hall_ID,mode,capacity,location),主码:(hall_ID) schedule(schedule_ID,date,time,price,number,hall_ID,movie_ID),主码:(schedule_ID);外码:(hall_ID,movie_ID) ticket(ticket_ID,seat_num,c_ID,schedule_ID),主码:(ticket_ID);外码:(c_ID,schedule_ID) ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:5:1","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"10.2 建模工具简介 1. ERWIN erwin Data Modeler行业领先的数据建模解决方案,具有直观的设计和存档功能,支持管理整个企业内任何存储位置的任何数据。但收费较贵,不过,大学学生可以申请erwin Data Modeler Academic Edition,有效期1年。可以去官网下载文档了解具体功能和使用方法: 中文官网:http://www.erwinchina.com/ 2.Navicat Navicat支持几乎所有你常用的DBMS,支持概念模型,逻辑模型和物理模型,可根据模型文件生成任何DBMS的脚本。支持Forward Engineering和Reverse Engineering。 该软件也是收费。 Navicat同时也是用户较多的DBMS客户端管理工具。 中文官网: https://www.navicat.com.cn/ **3.Microsoft Visio ** 现在,Visio已从Office分离出来,需要单独购买。支持陈氏E-R图和Crow’s foot。可用来表达概念模型,逻辑模型等。 4.Draw.io 这是最容易获得的建模工具,只需要web browser的地址栏里输入URL即可调出: draw.io 或者: https://app.diagrams.net/ 输入前者会自动跳转到后者。 比较适合建立概念模型和逻辑模型。既支持陈氏ER图,也支持Crow’s footsER图。但它不能跟具体的DBMS连接,不支持正向及逆向工程。 5. MySQL Workbench MySQL社区版自带的免费工具。也是比较好用的图形界面客户端管理工具。支持正向及逆向工具。 官网可下载使用手册。 ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:6:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Git"],"content":"基本流程 # 初始化仓库 git init # 将本地库关联至远程仓库 git remote add origin git@github.com:....github.io.git # 查看当前修改状态 git status # 添加修改过得文件, . 表示所有,也可以指定文件 git add . # \"\"里面的内容就是提交内容的说明信息 git commit -m \"first commit\" # 第一次提交方法1 git push -u -f origin main #第一次提交方法2 git pull origin main --allow-unrelated-histories git push -u origin main # 以后提交 git push ","date":"2022-10-21","objectID":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/:1:0","tags":["Git","Github","Gitee"],"title":"Git基本用法\u0026报错记录","uri":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/"},{"categories":["Git"],"content":"其他用法 1 修改分支名 git branch -m oldBranchName newBranchName 2 取消与远程仓库的关联 git remote remove origin 3 实现本地库同时关联GitHub和Gitee # 初始化仓库 git init # 将本地库同时和GitHub、Gitee的远程仓库关联 git remote add github git@github.com:bertilchan/gitTest.git git remote add gitee git@gitee.com:bertil/git-test.git # 查看关联的远程库信息 git remote -v # 添加修改,和原来一样 git add . git commit -m \"first commit\" # 分别提交 git push -u github main git push -u gitee main ","date":"2022-10-21","objectID":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/:2:0","tags":["Git","Github","Gitee"],"title":"Git基本用法\u0026报错记录","uri":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/"},{"categories":["Git"],"content":"报错记录 ","date":"2022-10-21","objectID":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/:3:0","tags":["Git","Github","Gitee"],"title":"Git基本用法\u0026报错记录","uri":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/"},{"categories":["Git"],"content":"报错1 ssh: Could not resolve hostname github.com: Temporary failure in name resolution fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. 解决方案见:Git报错-ssh相关错误 ","date":"2022-10-21","objectID":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/:3:1","tags":["Git","Github","Gitee"],"title":"Git基本用法\u0026报错记录","uri":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/"},{"categories":["Git"],"content":"报错2 push后GitHub文件夹出现箭头且无法打开 解决方案见:Git报错-GitHub文件夹出现箭头且无法打开 ","date":"2022-10-21","objectID":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/:3:2","tags":["Git","Github","Gitee"],"title":"Git基本用法\u0026报错记录","uri":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/"},{"categories":["Go"],"content":"1 路由与控制器 ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:1:0","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"1 路由规则 一条路由规则由:http请求方法 , url路径 , 控制器函数 组成 1.http请求方法 GET POST PUT DELETE 2.url路径 静态url路径 带路径参数的url路径 带星号(*)模糊匹配参数的url路径 // 例子1, 静态Url路径, 即不带任何参数的url路径 /users/center /user/101 /food/100 // 例子2,带路径参数的url路径,url路径上面带有参数,参数由冒号(:)跟着一个字符串定义。 // 路径参数值可以是数值,也可以是字符串 //定义参数:id, 可以匹配/user/1, /user/899 /user/xiaoli 这类Url路径 /user/:id //定义参数:id, 可以匹配/food/2, /food/100 /food/apple 这类Url路径 /food/:id //定义参数:type和:page, 可以匹配/foods/2/1, /food/100/25 /food/apple/30 这类Url路径 /foods/:type/:page // 例子3. 带星号(*)模糊匹配参数的url路径 // 星号代表匹配任意路径的意思 //匹配:/foods/1, /foods/200, /foods/1/20, /foods/apple/1 //以/foods/ 开头的所有路径都匹配 /foods/* 3.url路径匹配顺序 如果出现,一个http请求路径匹配多个定义的url路径,echo框架按下面顺序匹配,先匹配到那个就用那个定义。 匹配静态url路径 匹配带路径参数的url路径 匹配带星号(*)模糊匹配参数的url路径 4.控制器函数 控制器函数接受一个上下文参数,并返回一个错误。可以通过上下文参数,获取http请求参数,响应http请求。 func HandlerFunc(c echo.Context) error{} ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:1:1","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"2 示例 实际项目开发中不要把路由定义和控制器函数都写在一个go文件,不方便维护。 //实例化echo对象。 e := echo.New() //定义post请求, url路径为:/users, 绑定saveUser控制器函数 e.POST(\"/users\", saveUser) //定义get请求,url路径为:/users/:id (:id是参数,例如: /users/10, 会匹配这个url模式),绑定getUser控制器函数 e.GET(\"/users/:id\", getUser) //定义put请求 e.PUT(\"/users/:id\", updateUser) //定义delete请求 e.DELETE(\"/users/:id\", deleteUser) //控制器函数实现 func saveUser(c echo.Context) error { ...忽略实现... } func getUser(c echo.Context) error { ...忽略实现... } func updateUser(c echo.Context) error { ...忽略实现... } func deleteUser(c echo.Context) error { ...忽略实现... } ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:1:2","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"2 处理请求参数 ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:2:0","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"1 绑定数据 通过将请求参数绑定到一个struct对象的方式获取数据。这种方式获取请求参数支持json、xml、k/v键值对等多种方式。 // User 结构体定义 type User struct { Name string `json:\"name\" form:\"name\" query:\"name\"` Email string `json:\"email\" form:\"email\" query:\"email\"` } 控制器代码: // Handler func(c echo.Context) (err error) { u := new(User) //调用echo.Context的Bind函数将请求参数和User对象进行绑定。 if err = c.Bind(u); err != nil { return } //请求参数绑定成功后 u 对象就保存了请求参数。 //这里直接将请求参数以json格式显示 //注意:User结构体,字段标签定义中,json定义的字段名,就是User对象转换成json格式对应的字段名。 return c.JSON(http.StatusOK, u) } ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:2:1","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"2 获取post请求数据 通过echo.Context对象的 FormValue 函数可以直接获取post请求参数。 通过FormValue函数获取参数的值,数据类型都是String类型, 如果需要其他类型的数据,需要自己转换数据格式。 // Handler func(c echo.Context) error { //获取name参数 name := c.FormValue(\"name\") //直接输出name参数 return c.String(http.StatusOK, name) } ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:2:2","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"3 获取get请求数据 通过echo.Context对象的 QueryParam 函数可以直接获取get请求参数。 // Handler func(c echo.Context) error { //获取name参数, 通过QueryParam获取的参数值也是String类型。 name := c.QueryParam(\"name\") //直接输出name参数 return c.String(http.StatusOK, name) }) ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:2:3","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"4 获取path路径参数 通过echo.Context对象的 Param 获取,url路径参数。 //例子: url路由规则为/users/:name , :name为参数。 e.GET(\"/users/:name\", func(c echo.Context) error { //获取路径参数:name的值 name := c.Param(\"name\") //如果请求url为: /users/tizi365 则name的值为tizi365 //Param获取的值也是String类型 return c.String(http.StatusOK, name) }) ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:2:4","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"3 处理请求结果 1 以字符串方式响应 String(code int, s string) error 2 以json格式响应 JSON(code int, i interface{}) error 3 以xml格式响应 XML(code int, i interface{}) error 4 以文件格式响应 5 设置http响应头 func(c echo.Context) error { //设置http响应 header c.Response().Header().Add(\"tizi\", \"tizi365\") return c.String(200, \"欢迎访问tizi360.com!\") } ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:3:0","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"4 访问静态资源文件 echo通过static中间件支持静态资源文件的访问。我们可以通过echo.Static函数初始化static中间件。 Static(prefix, root string) *Route //初始化echo实例 e := echo.New() //设置Static中间件 //如果我们访问 /res/tizi.jpg这个url路径,实际上就是访问static/tizi.jpg这个路径的内容 e.Static(\"/res\", \"static\") 我们也可以通过Echo.File函数为一个url地址绑定一个静态资源文件。 //初始化echo实例 e := echo.New() //访问 / 就是访问public/index.html文件, index.html相当于站点默认首页 e.File(\"/\", \"public/index.html\") //访问/favicon.ico 就是访问images/favicon.ico文件, 相当于为站点设置了图标 e.File(\"/favicon.ico\", \"images/favicon.ico\") ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:4:0","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"在go语言标准库中,net包提供了可移植的网络I/O接口,包括TCP/IP、UDP、域名解析和Unix域socket。 ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:0:0","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"1 服务端 ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:1:0","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"1. 解析地址 在TCP服务端我们需要监听一个TCP地址,因此建立服务端前我们需要生成一个正确的TCP地址,这就需要用到 Resolve 函数。 // ResolveTCPAddr函数会输出一个TCP连接地址和一个错误信息 func ResolveTCPAddr(network, address string) (*TCPAddr, error) // 解析IP地址 func ResolveIPAddr(net, addr string) (*IPAddr, error) // 解析UDP地址 func ResolveUDPAddr(net, addr string) (*UDPAddr, error) // 解析Unix地址 func ResolveUnixAddr(net, addr string) (*UnixAddr, error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:1:1","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"2. 监听请求 我们可以通过 Listen 方法监听我们解析后的网络地址。 // 监听net类型,地址为laddr的地址 func Listen(net, laddr string) (Listener, error) // 监听TCP地址 func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error) // 监听IP地址 func ListenIP(netProto string, laddr *IPAddr) (*IPConn, error) // 监听UDP地址 func ListenMulticastUDP(net string, ifi *Interface, gaddr *UDPAddr) (*UDPConn, error) func ListenUDP(net string, laddr *UDPAddr) (*UDPConn, error) // 监听Unix地址 func ListenUnixgram(net string, laddr *UnixAddr) (*UnixConn, error) func ListenUnix(net string, laddr *UnixAddr) (*UnixListener, error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:1:2","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"3. 接受请求 TCPAddr 实现了两个接受请求的 Accept 方法,两者代码实现其实是一样的,唯一的区别是第一种返回了一个对象,第二种返回了一个接口。 func (l *TCPListener) AcceptTCP() (*TCPConn, error) func (l *TCPListener) Accept() (Conn, error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:1:3","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"4. 连接配置 // 配置监听器超时时间:超过t之后监听器自动关闭,0表示不设置超时时间 func (l *TCPListener) SetDeadline(t time.Time) error // 关闭监听器 func (l *TCPListener) Close() error ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:1:4","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"5. 编写一个服务器 func main() { // 解析服务端监听地址,本例以tcp为例 addr, err := net.ResolveTCPAddr(\"tcp\", \"127.0.0.1:8000\") if err != nil { log.Panic(err) } // 创建监听器 listen, err := net.ListenTCP(\"tcp\", addr) if err != nil { log.Panic(err) } for { // 监听客户端连接请求 conn, err := listen.AcceptTCP() if err != nil { continue } // 处理客户端请求 这个函数可以自己编写 go HandleConnectionForServer(conn) } } ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:1:5","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"2 客户端 ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:2:0","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"1. 解析地址 在TCP服务端我们需要监听一个TCP地址,因此建立服务端前我们需要生成一个正确的TCP地址,这就需要用到 Resolve 函数了。 // ResolveTCPAddr函数会输出一个TCP连接地址和一个错误信息 func ResolveTCPAddr(network, address string) (*TCPAddr, error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:2:1","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"2. 发送连接请求 // DialIP的作用类似于IP网络的拨号 func DialIP(network string, laddr, raddr *IPAddr) (*IPConn, error) // Dial 连接到指定网络上的地址,涵盖 func Dial(network, address string) (Conn, error) // 这个方法只是在Dial上面设置了超时时间 func DialTimeout(network, address string, timeout time.Duration) (Conn, error) // DialTCP 专门用来进行TCP通信的 func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error) // DialUDP 专门用来进行UDP通信的 func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error) // DialUnix 专门用来进行 Unix 通信 func DialUnix(network string, laddr, raddr *UnixAddr) (*UnixConn, error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:2:2","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"3. 编写一个客户端 func main() { // 解析服务端地址 RemoteAddr, err := net.ResolveTCPAddr(\"tcp\", \"127.0.0.1:8000\") if err != nil { panic(err) } // 解析本地连接地址 LocalAddr, err := net.ResolveTCPAddr(\"tcp\", \"127.0.0.1\") if err != nil { panic(err) } // 连接服务端 conn, err := net.DialTCP(\"tcp\", LocalAddr, RemoteAddr) if err != nil { panic(err) } // 连接管理 HandleConnectionForClient(conn) } ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:2:3","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"3 域名解析 ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:3:0","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"1. DNS正向解析 CNAME 被称为规范名字。这种记录允许您将多个名字映射到同一台计算机。 通常用于同时提供WWW和MAIL服务的计算机。 //域名解析到cname func LookupCNAME(name string) (cname string, err error) //域名解析到地址 func LookupHost(host string) (addrs []string, err error) //域名解析到地址[]IP结构体.可以对具体ip进行相关操作(是否回环地址,子网,网络号等) func LookupIP(host string) (addrs []IP, err error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:3:1","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"2. DNS反向解析 // 根据ip地址查找主机名地址(必须得是可以解析到的域名)[dig -x ipaddress] func LookupAddr(addr string) (name []string, err error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:3:2","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"4 其他常用接口 ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:4:0","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"1. Conn接口 type Conn interface { // Read从连接中读取数据 // Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真 Read(b []byte) (n int, err error) // Write从连接中写入数据 // Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真 Write(b []byte) (n int, err error) // Close方法关闭该连接 // 并会导致任何阻塞中的Read或Write方法不再阻塞并返回错误 Close() error // 返回本地网络地址 LocalAddr() Addr // 返回远端网络地址 RemoteAddr() Addr // 设定该连接的读写deadline,等价于同时调用SetReadDeadline和SetWriteDeadline // deadline是一个绝对时间,超过该时间后I/O操作就会直接因超时失败返回而不会阻塞 // deadline对之后的所有I/O操作都起效,而不仅仅是下一次的读或写操作 // 参数t为零值表示不设置期限 SetDeadline(t time.Time) error // 设定该连接的读操作deadline,参数t为零值表示不设置期限 SetReadDeadline(t time.Time) error // 设定该连接的写操作deadline,参数t为零值表示不设置期限 // 即使写入超时,返回值n也可能\u003e0,说明成功写入了部分数据 SetWriteDeadline(t time.Time) error } ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:4:1","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"2. PacketConn接口 type PacketConn interface { // ReadFrom方法从连接读取一个数据包,并将有效信息写入b // ReadFrom方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真 // 返回写入的字节数和该数据包的来源地址 ReadFrom(b []byte) (n int, addr Addr, err error) // WriteTo方法将有效数据b写入一个数据包发送给addr // WriteTo方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真 // 在面向数据包的连接中,写入超时非常罕见 WriteTo(b []byte, addr Addr) (n int, err error) // Close方法关闭该连接 // 会导致任何阻塞中的ReadFrom或WriteTo方法不再阻塞并返回错误 Close() error // 返回本地网络地址 LocalAddr() Addr // 设定该连接的读写deadline SetDeadline(t time.Time) error // 设定该连接的读操作deadline,参数t为零值表示不设置期限 // 如果时间到达deadline,读操作就会直接因超时失败返回而不会阻塞 SetReadDeadline(t time.Time) error // 设定该连接的写操作deadline,参数t为零值表示不设置期限 // 如果时间到达deadline,写操作就会直接因超时失败返回而不会阻塞 // 即使写入超时,返回值n也可能\u003e0,说明成功写入了部分数据 SetWriteDeadline(t time.Time) error } ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:4:2","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"3. Error接口 package net type Error interface{ Timeout() bool // 错误是否超时 Temporary() bool // 是否是临时错误 } ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:4:3","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"5 示例 server.go /*服务器代码*/ package main import ( \"fmt\" \"net\" ) func main() { //创建listener listener, err := net.Listen(\"tcp\", \"localhost:50000\") //用来监听和接收来自客户端的请求 if err != nil { fmt.Println(\"Error Listening\", err.Error()) //Error是个什么? return //终止程序 } //无限循环,监听并接受来自客户端的连接 for { conn, err := listener.Accept() if err != nil { fmt.Println(\"Error accepting\", err.Error()) return //终止程序 } go doServerStuff(conn) //? } } func doServerStuff(conn net.Conn) { for { buf := make([]byte, 512) len, err := conn.Read(buf) //获取客户端发送字节数 if err != nil { fmt.Println(\"Error reading\", err.Error()) return //终止程序 } fmt.Printf(\"Received data:%v\\n\", string(buf[:len])) } } client.go package main import ( \"bufio\" \"fmt\" \"net\" \"os\" \"strings\" ) func main() { //创建和服务器的连接 conn, err := net.Dial(\"tcp\", \"localhost:50000\") if err != nil { fmt.Println(\"Error dialing\", err.Error()) //由于目标计算机积极拒绝而无法创建连接 return //终止程序 } inputReader := bufio.NewReader(os.Stdin) //接收来自键盘的输入 fmt.Println(\"First,what is your name?\") clientName, _ := inputReader.ReadString('\\n') trimmedClient := strings.Trim(clientName, \"\\r\\n\") // Windows 平台下用 \"\\r\\n\",Linux平台下使用 \"\\n\" //给服务器发送信息知道程序退出 for { fmt.Println(\"What to send to the server? Type Q to quit.\") input, _ := inputReader.ReadString('\\n') trimmedInput := strings.Trim(input, \"\\r\\n\") if trimmedInput==\"Q\"{ return } _,err=conn.Write([]byte(trimmedClient+\" says: \"+trimmedInput)) } } socket.go package main import( \"fmt\" \"io\" \"net\" ) func main(){ var( host=\"www.apache.org\" port=\"80\" remote=host+\":\"+port msg string=\"GET / \\n\" data=make([]uint8,4096) read=true count=0 ) //创建一个socket conn,err:=net.Dial(\"tcp\",remote) //发送我们的消息:一个http GET请求 io.WriteString(conn,msg) //读取服务器的响应 for read{ count,err=conn.Read(data) read=(err==nil) fmt.Printf(string(data[0:count])) } conn.Close() } dial.go //make a connection with www.example.org: package main import( \"fmt\" \"net\" \"os\" ) func main(){ conn,err:=net.Dial(\"tcp\",\"192.0.32.10:80\") //tcp ipv4 checkConnection(conn,err) conn,err=net.Dial(\"udp\",\"192.0.32.10:80\") //udp checkConnection(conn,err) conn,err=net.Dial(\"tcp\",\"[2620:0:2d0:200::10]:80\") //tcp ipv6 checkConnection(conn,err) } func checkConnection(conn net.Conn,err error){ if err!=nil{ fmt.Printf(\"error %v connecting!\",err) os.Exit(1) } fmt.Printf(\"Connection is made with %v\\n\",conn) } server_simple.go package main import( \"flag\" \"fmt\" \"net\" \"syscall\" ) const maxread=25 func main(){ flag.Parse() //服务器地址和端口通过命令行传入参数,并通过flag包来读取这些参数 if flag.NArg() != 2 { //检查是否按照期望传入了2个参数 panic(\"usage: host port\") //此函数停止执行,并将控制权返还给其调用者 } hostAndPort := fmt.Sprintf(\"%s:%s\", flag.Arg(0), flag.Arg(1)) //格式化成字符串 listener := initServer(hostAndPort) for { conn, err := listener.Accept() //接受请求,返回conn对象 checkError(err, \"Accept: \") go connectionHandler(conn) } } func initServer(hostAndPort string)net.Listener{ serverAddr,err:=net.ResolveTCPAddr(\"tcp\",hostAndPort) //解析TCP地址 checkError(err,\"Resolving address:port failed: '\"+hostAndPort+\"'\") listener,err:=net.ListenTCP(\"tcp\",serverAddr) //监听请求 checkError(err,\"ListenTCP: \") println(\"Listening to: \",listener.Addr().String()) return listener } func connectionHandler(conn net.Conn){ connFrom:=conn.RemoteAddr().String() //获取客户端的地址 println(\"Connection from: \",connFrom) sayHello(conn) for{ var ibuf []byte=make([]byte, maxread+1) //设置maxread防止溢出 length,err:=conn.Read(ibuf[0:maxread]) //读取连接中的内容 ibuf[maxread]=0 switch err{ case nil: handleMsg(length,err,ibuf) case syscall.EAGAIN: continue //重新尝试连接 default: goto DISCONNECT } } DISCONNECT: err:=conn.Close() //关闭连接 println(\"Closed connection: \",connFrom) checkError(err,\"Close: \") } func sayHello(to net.Conn) { obuf := []byte{'L', 'e', 't', '\\'', 's', ' ', 'G', 'O', '!', '\\n'} wrote, err := to.Write(obuf) //发送message给客户端 checkError(err, \"Write: wrote \"+string(wrote)+\" bytes.\") } func handleMsg(length int, err error, msg []byte) { if length \u003e 0 { print(\"\u003c\", length, \":\") for i := 0; ; i++ { if msg[i] == 0 { break } fmt.Printf(\"%c\", msg[i]) } print(\"\u003e\") } } func checkError(error error, info string) { if error !=","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:5:0","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["深度学习"],"content":"本文为论文 Vision GNN: An Image is Worth Graph of Nodes 的阅读笔记。 论文下载:https://arxiv.org/abs/2206.00272 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:0:0","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"引言 网络架构在基于深度学习的计算机视觉中起着关键作用。广泛使用的CNN和 transformer(变换器)将图像视为 grid(网格)或 sequence(序列)结构,这对于捕捉不规则、复杂的物体来说是不灵活的。本文建议将图像表示为一个 graph 结构,并引入一个新的 Vision GNN(ViG)架构来提取视觉任务的图层特征。 文章主要工作: 介绍了计算机视觉方面的现有模型方法和成果 介绍ViG模型的构建过程及工作原理,为未来的研究提供有用的灵感和经验 通过图像分类和物体检测实验证明了ViG模型在视觉任务中的有效性 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:1:0","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"1 相关研究 CNN 曾经是计算机视觉中标准的网络结构,但近来 transformer with attention mechanism 、MLP-based 等模型也在不断发展,这些正在将视觉模型推向一个前所未有的高度。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:2:0","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"1.1 3种图像结构 不同的网络结构以不同的方式处理输入的图像,通常有grid, sequence ,graph 3种,如下图所示。在 grid 和 sequence 结构中,节点只按空间位置排序;在 graph 结构中,节点是通过其内容连接的,不受局部位置的限制。 CNN 在图像上应用滑动窗口,并引入移位变异性和位置性;最近的 vision transformer 或 MLP 将图像视为 a sequence of patches(补丁序列)。 由于物体形状通常不是规则的四边形,常用的 grid 或 sequence 结构处理起图像来不够灵活,所以在本文中采用 graph 结构。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:2:1","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"1.2 3种模型 CNN:曾经是计算机视觉中的主流网络结构,已经成功地应用于各种视觉任务,如图像分类、物体检测和语义分割。CNN模型在过去的十年里发展迅速,代表性的模型包括ResNet、MobileNet和NAS。 Vision transformer:从2020年开始,被引入到视觉任务中,ViT的一些变体开始被提出来以提高视觉任务的性能。主要的改进包括金字塔结,局部注意和位置编码。 MLP:通过专门设计的模块,MLP可以达到有竞争力的性能,并且在一般的视觉任务(如物体检测和分割)上工作。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:2:2","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"1.3 GNN模型 1. GNN\u0026GCN GNN:图神经网络,由于传统的CNN网络无法表示顶点和边这种关系型数据,便出现了图神经网络解决这种图数据的表示问题,这属于CNN往图方向的应用扩展 GCN:图卷积神经网络,GNN在训练过程中,有将attention引入图结构的,有将门控机制引入图结构的,还有将卷积引入图结构的,引入卷积的GNN就是GCN,通过提取空间特征来进行学习 2. 发展 Micheli提出了早期的提出了早期的基于空间的GCN,Bruna等人提出了基于频谱的GCN,近几年来基于这两种GCN的改进和扩展也被提出。 3. 应用 GCN通常被应用于图数据,如社会网络、引文网络和生化图;在计算机视觉领域的应用主要包括点云分类、场景图生成和动作识别。 GCN只能解决自然形成的图的特定视觉任务,对于计算机视觉的一般应用,我们需要一个基于GCN的骨干网络来直接处理图像数据。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:2:3","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"2 ViG模型 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:3:0","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"2.1 模型构建 Image→Graph 首先基于 graph 结构建立视觉图形神经网络,用于视觉任务。将输入的图像划分为若干个 patches(补丁),并将每个斑块视为一个 node (节点)。1 . 对于一个大小为 $H×W×3$ 的图像,我们将其分为 N 个补丁,把每个补丁转化为一个特征向量 $x_i∈R^D$,得到 $X = [x_1,x_2,…,x_N ]$, 其中 $D$ 是特征维度。将特征看做无序节点$V={v_1,v_2,…,v_N}$,节点$v_i$的k邻近节点记为$N(v_i)$,对每个$v_j∈N(v_i)$添加$v_j$到$v_i$的边$e_ji$。 最终得到图$G = (V,E) $,我们把图的构建过程记为$G = G(X)$。 图层处理 图卷积层可以通过聚合其邻居节点的特征在节点之间交换信息。具体操作方式为: $G' = F(G, W)=Update(Aggregate(G, W_agg), W_update) $ 其中,$W_agg$和 $W_update$是聚合、更新操作的可学习权重。 聚合:通过聚合邻居节点的特征来计算一个节点的表征 更新:进一步合并聚合的特征 通过最大相对卷积处理图层面,记为$X' = GraphConv(X)$。 $x_i' = h(x_i, g(x_i, N(x_i), W_agg), W_update)$ $g(·) = x_i'' = [x_i, max(${$x_j - x_i|j∈N(x_i)$}] $h(·) = x_i' = x_i'‘W_update$ . 接着进行图卷积的多头更新操作(有利于特征多样性),将聚合后的特征 $x_i’'$ 分割成 $h$ 个头,然后分别以不同的权重更新这些头,得到: $x_i' = [head^1W^1update, head^2W^2update,…head^hW^hupdate]$ ViG block ViG的2个基本模块 Graph模块:是在图卷积的基础上构建的,用于聚合和更新图形信息,可以缓解传统GNN的过度平滑现象 FFN模块:带有两个线性层,用于节点特征转换和鼓励节点多样性 以前的GCN通常重复使用卷积层来提取图形数据的聚合特征,这会导致过度平滑的现象 ,降低节点特征的显著性,如下图所示所示。为了解决这个问题,本文在ViG块中引入了更多的特征转换和非线性激活。 我们在图卷积前后应用一个线性层,将节点特征投射到同一领域,增加特征多样性。在图卷积之后插入一个非线性激活函数以避免层崩溃。我们称升级后的模块为Grapher模块,给定输入特征$X∈R^N×^D$ ,则可得到:$Y = σ(GraphConv(XW_in))W_out + X$ 2 . 其中$W_in$和$W_out$是全连接层的权重,σ是激活函数。为了进一步提高特征转换能力,我们在每个节点上利用前馈网络(FFN):$Z = σ(YW_1)W_2 + Y$ 其中$W_1$和$W_2$是全连接层的权重。Graph模块和FFN模块的堆叠构成了ViG块,作为构建网络的基本单元。基于图像的graph结构和ViG块,我们可以建立ViG网络。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:3:1","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"2.2 网络框架 各项同性结构:指主体在整个网络中具有同等大小和形状的特征 金字塔结构:考虑了图像的多尺度特性,提取特征的空间大小逐渐变小 在计算机视觉领域,常用的结构有各向同性结构和金字塔结构。为了与其他类型的神经网络有更普遍的比较,文章分别为ViG建立了这两种网络结构。 各向同性结构 文章建立了3个大小不同的各向同性ViG架构。为了扩大感受野,邻居结点的数量K从9线性增加到18;头的数量被设定为 h = 4。详情如下表:3 金字塔结构 文章建立了4个大小不同的金字塔ViG模型。详情如下:4 位置编码 为了表示节点的位置信息,文章为每个节点特征添加一个位置向量:$x_i←x_i+e_i$ ;金字塔结构中可以进一步使用相对位置编码。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:3:2","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"2.3 模型优点 graph 是广义的数据结构,grid 和 sequence 可以看做 graph 的特例 graph 更灵活,可以对复杂、不规则的物体进行建模 一个物体可以被看作是由各个部分组成的,graph 结构可以构建他们的联系 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:3:3","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"3 实验 top1 accuracy:预测的label取最后概率向量里面最大的那一个作为预测结果,如果预测结果中概率最大的分类正确,则预测正确,否则预测错误。 top5 accuracy:最后概率向量最大的前五名中,只要出现了正确概率即为预测正确,否则预测错误。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:4:0","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"3.1 实验结果 本文分别将各向同性结构、金字塔结构的ViG与同样结构的CNN、转化器和 MLPs对比,可以看出: 将图片视作Graph能够在计算机视觉任务中取得非常好的结果 和各向同性结构相比,金字塔结构的ViG具有更好的性能 各向同性结构的实验结果: 金字塔结构的实验结果: ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:4:1","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"3.2 消融研究 消融研究:通过删除部分网络并研究网络的性能来更好的了解网络。 文章以各向同性的ViG-Ti为基础架构,在ImageNet分类任务上进行了消融研究,结果如下: 通过改变图卷积的类型,发现不同图卷积的Top-1准确率很高,说明ViG架构的灵活性。其中,最大相对卷积在计算量和精度之间实现了最佳的权衡。 直接利用图卷积进行图像分类的效果很差,可以通过添加更多的特征转换,如引入FC和FFN不断提高准确性。 太少的邻居结点会降低信息交流,太多会导致过度平滑。当邻居节点的数量在9-15的范围时表现较好。 头的数量 h=4时,计算量和精度可以最好平衡。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:4:2","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"3.3 可视化 为了更好地理解本文的ViG模型是如何工作的,作者可视化了构建的图结构,展示了两个不同深度的样本的图。五角星是中心节点,相同颜色的节点是其邻居。 可以看到,在浅层,邻居节点往往是根据低层次、局部特征来选择的,如颜色和纹理;在深层层中,中心节点的邻居更具语义性,属于同一类别。而本文的ViG网络可以通过其内容和语义表征逐渐将节点联系起来,并帮助更好地识别物体。 参考资料: 从图(Graph)到图卷积(Graph Convolution):漫谈图神经网络模型 (一) 图卷积神经网络(GCN) GRAPH CONVOLUTIONAL NETWORKS 不用像素当做节点的原因:会导致节点过多 ↩︎ 最后加上 $X$​ 是残差连接,为了避免过拟合。 ↩︎ FLOPs:浮点运算数,可以用来衡量算法/模型的复杂度。 ↩︎ E是FNN中的隐藏维度 ↩︎ ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:4:3","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["Go"],"content":"1 结构 go run helloworld.go:执行Go代码 go build helloworld.go:编译生成二进制文件 ./helloworld:运行 import 声明必须跟在文件的 package 声明之后 Go 语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句 函数的左括号 { 必须和 func 函数声明在同一行上,且位于末尾,不能独占一行 在表达式 x+y 中,可在 + 后换行,不能在 + 前换行 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:1:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"2 基础语法 //格式化字符串 var stockcode = 123 var enddate = \"2020-12-31\" var url = \"Code=%d\u0026endDate=%s\" var target_url = fmt.Sprintf(url, stockcode, enddate) fmt.Println(target_url) ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:2:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"3 语言类型 布尔型 数字型 整形:int uint 浮点型:float complex 字符串 派生类型 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:3:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"4 变量 变量声明 var identifier type(指定变量类型,如果没有初始化,则变量默认为零值 var v_name = value(根据值自行判断变量类型 v_name := value(只能在函数体中出现 // 这种因式分解关键字的写法一般用于声明全局变量 var ( vname1 v_type1 vname2 v_type2 ) 局部变量不允许声明但不使用,全局变量可以 a, b = b, a (简单交换2个变量 _:空白标识符,也用于被抛弃值 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:4:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"5 常量 const identifier [type] = value //用作枚举 const ( Unknown = 0 Female = 1 Male = 2 ) 常量可以用len(), cap(), unsafe.Sizeof()函数计算表达式的值(必须是内置函数 iota 在const关键字出现时将被重置为0,const中每新增一行常量声明将使 iota 计数一次 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:5:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"6 条件语句 switch 匹配项后面也不需要再加 break fallthrough 执行后面的case case后面是类型不被局限于常量或整数,可以加多个,必须类型相同 func main() { var grade string = \"B\" var marks int = 90 switch marks { case 90: grade = \"A\" case 80,70: grade = \"B\" default: grade = \"D\" } switch { case grade == \"A\": fmt.Println(\"youxiu\") case grade == \"B\", grade == \"C\": fmt.Println(\"lianghao\") default: fmt.Println(\"cha\") } } type switch 判断某个 interface 变量中实际存储的变量类型 select 通信的 switch 语句 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:6:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"7 循环语句 for循环 for init; condition; post { } for condition { } for { } //range格式可以对 slice、map、数组、字符串等进行迭代循环 for key, value := range oldMap { newMap[key] = value } for key := range oldMap for _, value := range oldMap 在多重循环中,可以用标号 label 标出想 break 的循环 在多重循环中,可以用标号 label 标出想 continue 的循环 goto 语句可以无条件地转移到过程中指定的行 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:7:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"8 函数 func function_name( [parameter list] ) [return_types] { 函数体 } 函数可作为实参 匿名函数,可作为闭包 //方法 func (variable_name variable_data_type) function_name() [return_type]{ /* 函数体*/ } ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:8:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"9 变量作用域 局部变量:作用域只在函数体内 全局变量:整个包甚至外部包(被导出后)使用 全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:9:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"10 数组 var variable_name [SIZE] variable_type 可以使用 ... 代替数组的长度 // 将索引为 1 和 3 的元素初始化 balance := [...]float32{1:2.0,3:7.0} 多维数组 使用 append() 函数向空的二维数组添加两行一维数组 //多维数组 var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type 可以创建各个维度元素数量不一致的多维数组 //向函数传递数组 void myFunction(param [10]int) { ... } ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:10:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"11 指针 var var_name *var-type 指针数组 var ptr [MAX]*int; 指向指针的指针 var ptr **int; ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:11:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"12 结构体 //定义结构体 type struct_variable_type struct { member definition ... member definition } //声明变量 variable_name := structure_variable_type {value1, value2...valuen} variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen} 访问结构体:结构体.成员名 结构体作为函数参数 结构体指针 //声明 var struct_pointer *Books 结构体指针用 . 访问结构体成员 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:12:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"13 切片(Slice) var identifier []type //定义切片 var slice1 []type = make([]type, len) //创建切片 make([]T, length, capacity) //capacity指定容量,为可选参数 s :=[] int {1,2,3 } //直接初始化切片 s := arr[:] //初始化切片 s,是数组 arr 的引用 s := arr[startIndex:endIndex] //将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片 len() 方法获取长度 cap() 可以测量切片最长可以达到多少 空切片(nil):切片未初始化,默认为nil,长度为0 copy() 方法拷贝切片 append() 方法向切片追加新元素 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:13:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"14 范围(range) 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素 在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对 //读取key,value for key, value := range oldMap { newMap[key] = value } //只读取key for key := range oldMap //只读取value for _, value := range oldMap range也可以用来枚举 Unicode 字符串 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:14:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"15 集合(Map) 无序的键值对的集合 /* 声明变量,默认 map 是 nil */ var map_variable map[key_data_type]value_data_type /* 使用 make 函数 */ map_variable := make(map[key_data_type]value_data_type) delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:15:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"16 接口 /* 定义接口 */ type interface_name interface { method_name1 [return_type] method_name2 [return_type] ... method_namen [return_type] } /* 定义结构体 */ type struct_name struct { /* variables */ } /* 实现接口方法 */ func (struct_name_variable struct_name) method_name1() [return_type] { /* 方法实现 */ } ... func (struct_name_variable struct_name) method_namen() [return_type] { /* 方法实现*/ } ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:16:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"17 错误处理 type error interface { Error() string } func Sqrt(f float64) (float64, error) { if f \u003c 0 { return 0, errors.New(\"math: square root of negative number\") } // 实现 } ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:17:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"18 并发 //goroutine语法 go 函数名( 参数列表 ) 同一个程序中的所有 goroutine 共享同一个地址空间 通道(channel) 是用来传递数据的一个数据结构 通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯 操作符 \u003c- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道 //声明通道 ch := make(chan int) //设置发送缓冲区 ch := make(chan int, 100) //遍历通道 v, ok := \u003c-ch //关闭通道 cl 参考资料: Go语言教程 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:18:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"}] \ No newline at end of file +[{"categories":["C++"],"content":"1 简介 🟠 pair 容器将 2 个普通元素 first 和 second(C++ 基本数据类型、结构体、类自定的类型等)创建成一个新元素\u003cfirst, second\u003e。 🔵 使用需加上头文件:#include \u003cutility\u003e ","date":"2023-03-18","objectID":"/stlpair%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:1:0","tags":["C++","STL","pair"],"title":"【STL】pair容器用法","uri":"/stlpair%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"2 创建map容器 1️⃣ 调用 pair 容器类的默认构造函数。 pair \u003cstring, int\u003e pair1; 2️⃣ 在创建 pair 容器的同时,进行初始化。 pair \u003cstring, int\u003e pair2(\"语文\",90); 3️⃣ 利用先前已创建好的 pair 容器和拷贝构造函数,再创建一个新的 pair 容器。 pair \u003cstring, int\u003e pair3(pair2); ","date":"2023-03-18","objectID":"/stlpair%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:2:0","tags":["C++","STL","pair"],"title":"【STL】pair容器用法","uri":"/stlpair%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"3 常见用法 3.1 手动为 pair 对象赋值 pair1.first = \"数学\"; pair1.second = \"100\"; 3.2 比较2个pair对象 先比较 pair.first 元素的大小,如果相等则继续比较 pair.second 元素的大小。对于进行比较的 2 个 pair 对象,其对应的键和值的类型比较相同 3.3 swap() 函数 能够互换 2 个 pair 对象的键值对,其操作成功的前提是这 2 个 pair 对象的键和值的类型要相同。 ","date":"2023-03-18","objectID":"/stlpair%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:3:0","tags":["C++","STL","pair"],"title":"【STL】pair容器用法","uri":"/stlpair%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"🔴 【动态规划三要素】重叠子问题、最优子结构、状态转移方程 🟢 【思维框架】明确 base case → 明确「状态」→ 明确「选择」 → 定义 dp 数组/函数的含义。 # ⾃顶向下递归的动态规划 def dp(状态1, 状态2, ...): for 选择 in 所有可能的选择: # 此时的状态已经因为做了选择⽽改变 result = 求最值(result, dp(状态1, 状态2, ...)) return result # ⾃底向上迭代的动态规划 # 初始化 base case dp[0][0][...] = base case # 进⾏状态转移 for 状态1 in 状态1的所有取值: for 状态2 in 状态2的所有取值: for ... dp[状态1][状态2][...] = 求最值(选择1,选择2...) ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:0:0","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"1 【重叠子问题】 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:0","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"题目——斐波那契数 力扣 509. 斐波那契数 斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。 给定 n ,请计算 F(n) 。 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:1","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"1.1 暴力递归 int fib(int N) { if (N == 1 || N == 2) return 1; return fib(N - 1) + fib(N - 2); } 子问题个数: O(2^n)。解决⼀个子问题的时间:只有 f(n - 1) + f(n - 2) ⼀个加法操作, 时间为 O(1)。因此这个算法的时间复杂度为二者相乘,即 O(2^n)。 这种方法存在大量重复计算,即重叠子问题,我们需要想办法解决这个问题。 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:2","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"1.2 带备忘录的递归解法(自顶向下) int fib(int N) { // 备忘录全初始化为 0 int[] memo = new int[N + 1]; // 进⾏带备忘录的递归 return helper(memo, N); } int helper(int[] memo, int n) { // base case if (n == 0 || n == 1) return n; // 已经计算过,不⽤再计算了 if (memo[n] != 0) return memo[n]; memo[n] = helper(memo, n - 1) + helper(memo, n - 2); return memo[n]; } 在这种算法中,由于不存在冗余计算,子问题个数为 O(n),解决一个子问题的时间仍为 O(1)。 所以算法的时间复杂度是 O(n),和暴力算法相比提升很多。 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:3","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"1.3 dp 数组的迭代/递推解法(自低向上) int fib(int N) { if (N == 0) return 0; int[] dp = new int[N + 1]; // base case dp[0] = 0; dp[1] = 1; // 状态转移 for (int i = 2; i \u003c= N; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[N]; } 根据斐波那契数列的状态转移方程( f(n) = f(n-1) + f(n-2) ),当前状态只和之前的两个状态有关,其实并不需要 用数组来存储所有状态,只要存储当前状态的前两个就行。 所以,可以进一步优化,把空间复杂度降为 O(1): int fib(int n) { if (n == 0 || n == 1) { // base case return n; } // 分别代表 dp[i - 1] 和 dp[i - 2] int dp_i_1 = 1, dp_i_2 = 0; for (int i = 2; i \u003c= n; i++) { // dp[i] = dp[i - 1] + dp[i - 2]; int dp_i = dp_i_1 + dp_i_2; // 滚动更新 dp_i_2 = dp_i_1; dp_i_1 = dp_i; } return dp_i_1; } ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:4","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"2 【转移方程】 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:5","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"题目——零钱兑换 力扣 322. 零钱兑换 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。 你可以认为每种硬币的数量是无限的。 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:6","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"2.1 暴力递归 🔴 确定 base case :目标金额 amount 为 0 时算法返回 0,因为不需要任何硬币就已经凑出目标金额了。 🟡 确定「状态」 ,也就是子问题中会变化的变量。在本题中是目标金额 amount。 🟢 确定「选择」 ,也就是导致「状态」产生变化的行为。在本题中为所有硬币的面值。 🔵 明确 dp 函数/数组的定义 。即凑出目标金额所需的最少硬币数量。 int coinChange(int[] coins, int amount) { // 题⽬要求的最终结果是 dp(amount) return dp(coins, amount) } // 定义:要凑出⾦额 n,⾄少要 dp(coins, n) 个硬币 int dp(int[] coins, int amount) { // base case if (amount == 0) return 0; if (amount \u003c 0) return -1; int res = Integer.MAX_VALUE; for (int coin : coins) { // 计算⼦问题的结果 int subProblem = dp(coins, amount - coin); // ⼦问题⽆解则跳过 if (subProblem == -1) continue; // 在⼦问题中选择最优解,然后加⼀ res = Math.min(res, subProblem + 1); } return res == Integer.MAX_VALUE ? -1 : res; } 假设目标金额为 n,给定的硬币个数为 k,那么子问题个数在 k^n 这个数量级。而解决每个子问题的复杂度为 O(k),相乘得到总时间复杂度为 O(k^n)。 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:7","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"2.2 带备忘录的递归 int[] memo; int coinChange(int[] coins, int amount) { memo = new int[amount + 1]; // 备忘录初始化为⼀个不会被取到的特殊值,代表还未被计算 Arrays.fill(memo, -666); return dp(coins, amount); } int dp(int[] coins, int amount) { if (amount == 0) return 0; if (amount \u003c 0) return -1; // 查备忘录,防⽌重复计算 if (memo[amount] != -666) return memo[amount]; int res = Integer.MAX_VALUE; for (int coin : coins) { // 计算⼦问题的结果 int subProblem = dp(coins, amount - coin); // ⼦问题⽆解则跳过 if (subProblem == -1) continue; // 在⼦问题中选择最优解,然后加⼀ res = Math.min(res, subProblem + 1); } // 把计算结果存⼊备忘录 memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res; return memo[amount]; } 子问题数目为 O(n)。处理⼀个子问题的时间不变,仍是 O(k),总的时间复杂度是 O(kn)。 ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:8","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["labuladong的算法秘籍"],"content":"2.3 dp 数组的迭代解法 dp 数组的定义:当目标金额为 i 时,至少需要 dp[i] 枚硬币凑出。 int coinChange(int[] coins, int amount) { int[] dp = new int[amount + 1]; // 数组⼤⼩为 amount + 1,初始值也为 amount + 1 Arrays.fill(dp, amount + 1); // base case dp[0] = 0; // 外层 for 循环在遍历所有状态的所有取值 for (int i = 0; i \u003c dp.length; i++) { // 内层 for 循环在求所有选择的最⼩值 for (int coin : coins) { // ⼦问题⽆解,跳过 if (i - coin \u003c 0) { continue; } dp[i] = Math.min(dp[i], 1 + dp[i - coin]); } } return (dp[amount] == amount + 1) ? -1 : dp[amount]; } ","date":"2023-03-18","objectID":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/:1:9","tags":["labuladong","算法","动态规划"],"title":"【动态规划】动态规划核心框架","uri":"/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/"},{"categories":["蓝桥杯刷题记录"],"content":"6 跑步锻炼 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:1:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 小蓝每天都锻炼身体。 正常情况下,小蓝每天跑 1 千米。如果某天是周一或者月初(1 日),为了激励自己,小蓝要跑 2 千米。如果同时是周一或月初,小蓝也是跑 2 千米。 小蓝跑步已经坚持了很长时间,从 2000 年 1 月 1 日周六(含)到 2020 年 10 月 1 日周四(含)。请问这段时间小蓝总共跑步多少千米? ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:1:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:30min 🎯难度:💡 大小月记不清。大月:1、3、5、7、8、10、12 闰年的判断方法:year%4==0 \u0026\u0026 year%100!=0) || year%400==0 注意闰年的2月是29天,不是28天!(因为这个调了好久 bug 💥💢) ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:1:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 #include \u003ciostream\u003eusing namespace std; // 判断是否是闰年,是则返回 true,否则返回 false bool isRun(int year){ if((year%4==0 \u0026\u0026 year%100!=0) || year%400==0) return true; return false; } int main(){ // 每个月对应的天数 int mon1[13]={0,31,29,31,30,31,30,31,31,30,31,30,31}; // 闰年 int mon2[13]={0,31,28,31,30,31,30,31,31,30,31,30,31}; // 非闰年 int cnt=6; // 用于判断是否是周一 int sum=0; // 计算总数 for(int y=2000;y\u003c=2020;y++){ // 遍历每一年 for(int m=1;m\u003c=12;m++){ // 遍历每一月 if(isRun(y)){ // 闰年 for(int d=1;d\u003c=mon1[m];d++){ if(d==1 || cnt%7==1) // 如果是月初或者周一要额外加 1 sum++; sum++; cnt=(cnt+1)%7; // 更新 cnt 的值 // 遍历到最后一天 2020.10.01 结束 if(y==2020 \u0026\u0026 m==10 \u0026\u0026 d==1){ printf(\"%d\\n\",sum); return 0; } } } else{ // 不是闰年 for(int d=1;d\u003c=mon2[m];d++){ if(d==1 || cnt%7==1) // 如果是月初或者周一要额外加 1 sum++; sum++; cnt=(cnt+1)%7; // 更新 cnt 的值 } } } } return 0; } ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:1:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"7 蛇形填数 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:2:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 如下图所示,小明用从 1 开始的正整数“蛇形”填充无限大的矩阵。 1 2 6 7 15 ... 3 5 8 14 ... 4 9 13 ... 10 12 ... 11 ... ... 容易看出矩阵第二行第二列中的数是 5。请你计算矩阵中第 20 行第 20 列的数是多少? ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:2:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:15min 🎯难度:💡 小学找规律题,可手算🎉🎉 要看清题目所给的下标是从0开始还是从1开始!(因为题中的矩阵行列从0开始算,找了好久bug 💢💥 注意题目要求。比如这道题只求(20, 20)的数,就只用找坐标为(a, a)的元素的规律,而不用把所有的都找出 找规律要容易出错,要多算几个验证 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:2:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 🟠 把所有坐标的元素表达式都找出来 这种做法比较普适,缺点是可能会浪费宝贵的答题时间,并且规律较多的情况会把自己绕晕,很容易疏忽一两个点导致答案不对。😭😤 我们先把每次填充的同一条斜线上的元素看成一组,很容易发现一些规律: 这条斜线上有 n 个元素,对每个元素 (i, j) 都有:i + j = n + 1(i,j 都是从1开始的❗❗❗) 当 n 为奇数时,从下往上填充,也就是 j 从 1 开始,比如:(3, 1),(2, 2),(1, 3)。偶数则相反。 元素值 = 它是第几个被填充的 那么,要求位置为 (20, 20) 的元素值,我们只需要知道他是第几个被填充的。 首先 20+20=40,说明 (20, 20) 所在的斜线上有39个元素,每个元素的坐标和都为40。又由于 39 是个奇数,所以这条斜线填充的顺序为:(39,1),(38,2),(37,3)…(20,20)…(1,39)。 (20,20) 是这条斜线上的第 20 个元素。而在这条斜线被开始填充前,已经填充的元素数为:1+2+3+4+5...+38 = 741 。最终可以得到 (20,20) 是第几个被填充的: 741+20=261 。 🟡 只考虑坐标为 (a, a) 的元素 先自己手动补充后面的元素,坐标形式为 (a, a) 的元素值依次为:1, 5, 13, 25, 41…… 仔细分析可以发现:1=4×0+1;5=4×1+1;13=4×3+1;25=4×6+1;41=4×10+1。除开表达式的相同点,接下来只用找 1, 3, 6, 10 的规律:1=1;3=1+2;6=1+2+3;10=1+2+3+4。由此我们很容易得到坐标为 (a,a) 的元素值表达式:4×(1+2+3+...a-1)+1 。 带入 a=20 得到最终答案:261。 ⚠ 此处要注意的是,一定要多写几个检验❗❗❗只看 1, 5, 13, 25 找规律就很容易出错❗❗❗ ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:2:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"8 既约分数 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:3:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 如果一个分数的分子和分母的最大公约数是 11,这个分数称为既约分数。 例如 3/4, 1/8, 7/1,都是既约分数。 请问,有多少个既约分数,分子和分母都是 11 到 2020 之间的整数(包括 11 和 2020)? ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:3:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:5min 🎯难度:💡 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:3:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 填空题,没什么好说的,直接暴力解决啦~ #include \u003ciostream\u003eusing namespace std; int main(){ int sum=0; for(int i=1;i\u003c=2020;i++){ // 分子 for(int j=1;j\u003c=2020;j++){ // 分母 int flag=1; for(int k=2;k\u003c=2020;k++){ if(i%k==0 \u0026\u0026 j%k==0){ flag=0;break; } } sum+=flag; } } cout\u003c\u003csum; return 0; } ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:3:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"9 数字三角形 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:4:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 上图给出了一个数字三角形。从三角形的顶部到底部有很多条不同的路径。对于每条路径,把路径上面的数加起来可以得到一个和,你的任务就是找到最大的和。 路径上的每一步只能从一个数走到下一层和它最近的左边的那个数或者右 边的那个数。此外,向左下走的次数与向右下走的次数相差不能超过 1。 输入描述 输入的第一行包含一个整数 N (1≤ N ≤100),表示三角形的行数。 下面的 N 行给出数字三角形。数字三角形上的数都是 0 至 100 之间的整数。 输出描述 输出一个整数,表示答案。 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:4:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:没写出来 🎯难度:💡💡 【求矩阵路径最值问题】动态规划、开一个数组记录到每个位置的最大/小值 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:4:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 这道题想了很久毫无头绪,是10道题里唯一不会写的,看了评论区一个大佬的题解后醍醐灌顶。核心思想: 🔴 用一个数组 s[i][j] 记录从 a[1][1]到 a[i][j] 的路径上的数字和最大值。关注点在结果位置,是 【上一跳从哪来】 而不是 【下一跳往哪走】 。 🔵 遍历三角形数组,用动态规划更新矩阵 s。base case为: s[1][1]=a[1][1] 。状态转移方程为 s[i][j]=a[i][j]+max(s[i-1][j-1],s[i-1][j]) 。( s[i][j] 处的最大值要么是从 [i-1][j-1] 过来的,要么是从 [i-1][j] 过来的) 🟢 由于要满足向左、向右的步数差不超过1,最终结果只能是最中间的。考虑n的奇偶性,n为奇数时,输出矩阵s最后一行最中间的,即 s[n][n/2+1] ;n为偶数时,矩阵s最后一行最中间有2个元素,输出值最大的那个,即 max(s[n][n/2],s[n][n/2+1]) 。 ⚠ 这个思路真的强推,在很多动态规划的题目中都能应用上,例如今天刚好做的:931. 下降路径最小和 #include \u003ciostream\u003eusing namespace std; int main() { // 读取数据 // s[i][j]:从 a[1][1] 遍历到 a[i][j] 的最大和 int a[105][105]={0},s[105][105]={0},n; scanf(\"%d\", \u0026n); for(int i=1;i\u003c=n;i++){ for(int j=1;j\u003c=i;j++){ scanf(\"%d\",\u0026a[i][j]); } } // 计算最大和 s[1][1]=a[1][1]; for(int i=2;i\u003c=n;i++){ for(int j=1;j\u003c=i;j++){ s[i][j]=a[i][j]+max(s[i-1][j-1],s[i-1][j]); } } // n 是偶数且要满足向左、向右的步数差不超过 1,只能是中间两个 // n 是奇数且要满足向左、向右的步数差不超过 1,只能是最中间的那个 if(n%2==0) cout\u003c\u003cmax(s[n][n/2],s[n][n/2+1]); // 注意 s 是二维矩阵,不能写错成一维的 else cout\u003c\u003cs[n][n/2+1]; return 0; } ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:4:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"10 回文日期 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:5:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 2020 年春节期间,有一个特殊的日期引起了大家的注意:2020 年 2 月 2 日。因为如果将这个日期按 “yyyymmdd” 的格式写成一个 8 位数是 20200202,恰好是一个回文数。我们称这样的日期是回文日期。 给定一个 8 位数的日期,请你计算该日期之后下一个回文日期和下一个 ABABBABA 型的回文日期各是哪一天。 输入描述 输入包含一个八位整数 N,表示日期。 对于所有评测用例,10000101≤ N ≤89991231,保证 N 是一个合法日期的 8 位数表示。 输出描述 输出两行,每行 1 个八位数。第一行表示下一个回文日期,第二行表示下一个 ABABBABA 型的回文日期。 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:5:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"答题情况 ⏰解题耗时:60min 🎯难度:💡 ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:5:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 虽然官方给这道题贴的标签是 中等 ,但感觉难度一般,暴力穷举就好。不过确实很容易出错QAQ。 🔴 计算第1个回文日期时,即 ABCDDCBA 型,只用根据 ABCD 来枚举,需要确保两点:1️⃣ ABCDDCBA 的值需大于输入的 起始日期date 2️⃣ ABCDDCBA 需要是一个合法日期。注意 D 的值只能是0或1,可以减少枚举量。 🔵 计算第2个回文日期时,即 ABABBABA 型,只用根据 AB 来枚举,条件和上面类似。不过由于B只能取0或1,又只有输出第一个符合的回文日期,A的值要么是a,要么是a+1。(a是输入日期 date 的最高位) 🟢 判断是否是合法日期,只需把日期拆成年、月、日,看月、日的值是否合法,注意需要看年份是否是闰年。 #include \u003ciostream\u003eusing namespace std; // 用于判断输入的值是否是一个合格日期 bool isDate(int y,int m,int d){ int mon1[13]={0,31,29,31,30,31,30,31,31,30,31,30,31}; // 闰年 int mon2[13]={0,31,28,31,30,31,30,31,31,30,31,30,31}; // 非闰年 if((y%4==0 \u0026\u0026 y%100!=0) || y%400==0){ // 闰年 if(m\u003e=1 \u0026\u0026 m\u003c=12 \u0026\u0026 d\u003e=1 \u0026\u0026 d\u003c=mon1[m]) return true; } else{ // 非闰年 if(m\u003e=1 \u0026\u0026 m\u003c=12 \u0026\u0026 d\u003e=1 \u0026\u0026 d\u003c=mon2[m]) return true; } return false; } int main() { int date,fh=0,ye,mo,da; cin\u003e\u003edate; int a=date/10000000; int b=date/1000000%10; int c=date/100000%100; int d=date/10000%1000; // 寻找第一个回文串日期 int flag=0; for(int i=a;i\u003c=9;i++){ for(int j=0;j\u003c=3;j++){ for(int k=0;k\u003c=9;k++){ if(i*10000001+j*1000010+k*100100 + 11000 \u003c= date) continue; if(i*10000001+j*1000010+k*100100 \u003e date){ // d=0 if(isDate(i*1000+j*100+k*10, k, j*10+i)){ cout \u003c\u003c i*10000001+j*1000010+k*100100 \u003c\u003cendl; flag=1; break; } } if(isDate(i*1000+j*100+k*10+1, 10+k, j*10+i)){ // d=1 cout \u003c\u003c i*10000001+j*1000010+k*100100+11000 \u003c\u003cendl; flag=1; break; } } if(flag==1) break; } if(flag==1) break; } // 寻找第一个 ABABBABA型回文串 if(a*10000000+a*100000+a*100+a\u003edate) cout\u003c\u003c a*10000000+a*100000+a*100+a \u003c\u003cendl; else if(a*10000000+a*100000+a*100+a+1011010\u003edate \u0026\u0026 (a==1 || a==2)) cout\u003c\u003c a*10000000+a*100000+a*100+a+1011010 \u003c\u003cendl; else cout\u003c\u003c (a+1)*10000000+(a+1)*100000+(a+1)*100+a+1\u003c\u003cendl; return 0; } ","date":"2023-03-17","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/:5:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(下)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8B/"},{"categories":["labuladong的算法秘籍"],"content":"1 题目 实现一个计算器,功能如下: 1、输入一个字符串,可以包含 + - * /、数字、括号以及空格,你的算法返回运算结果。 2、要符合运算法则,括号的优先级最高,先乘除后加减。 3、除号是整数除法,无论正负都向 0 取整(5/2=2,-5/2=-2)。 4、可以假定输入的算式⼀定合法,且计算过程不会出现整型溢出,不会出现除数为 0 的意外情况。 ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/:1:0","tags":["labuladong","数据结构"],"title":"【数据结构设计】实现一个计算器","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/"},{"categories":["labuladong的算法秘籍"],"content":"2 解析 ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/:2:0","tags":["labuladong","数据结构"],"title":"【数据结构设计】实现一个计算器","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/"},{"categories":["labuladong的算法秘籍"],"content":"2.1 字符串转整数 // 把字符串s转为整数n int n = 0; for (int i = 0; i \u003c s.size(); i++) { char c = s[i]; n = 10 * n + (c - '0'); } ❗❗❗ 注意 (c - ‘0’) 的括号不能省略,否则可能造成整型溢出。 ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/:2:1","tags":["labuladong","数据结构"],"title":"【数据结构设计】实现一个计算器","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/"},{"categories":["labuladong的算法秘籍"],"content":"2.2 处理加减法 🟠 先给第⼀个数字加⼀个默认符号 +,变成 +1-12+3。 🟡 把⼀个运算符和数字组合成⼀对,也就是三对 +1,-12,+3,把它们转化成数字,然后放到⼀个栈 中。 🟢 将栈中所有的数字求和,就是原算式的结果。 int calculate(string s) { stack\u003cint\u003e stk; // 记录算式中的数字 int num = 0; // 记录 num 前的符号,初始化为 + char sign = '+'; for (int i = 0; i \u003c s.size(); i++) { char c = s[i]; // 如果是数字,连续读取到 num if (isdigit(c)) num = 10 * num + (c - '0'); // 如果不是数字,就是遇到了下⼀个符号, // 之前的数字和符号就要存进栈中 if (!isdigit(c) || i == s.size() - 1) { switch (sign) { case '+': stk.push(num); break; case '-': stk.push(-num); break; } // 更新符号为当前符号,数字清零 sign = c; num = 0; } } // 将栈中所有结果求和就是答案 int res = 0; while (!stk.empty()) { res += stk.top(); stk.pop(); } return res; } ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/:2:2","tags":["labuladong","数据结构"],"title":"【数据结构设计】实现一个计算器","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/"},{"categories":["labuladong的算法秘籍"],"content":"2.3 处理乘除法 思路跟仅处理加减法类似,其他部分都不变,只需要在 switch 部分加上对应的 case 。 for (int i = 0; i \u003c s.size(); i++) { char c = s[i]; if (isdigit(c)) num = 10 * num + (c - '0'); if (!isdigit(c) || i == s.size() - 1) { switch (sign) { int pre; case '+': stk.push(num); break; case '-': stk.push(-num); break; // 只要拿出前⼀个数字做对应运算即可 case '*': pre = stk.top(); stk.pop(); stk.push(pre * num); break; case '/': pre = stk.top(); stk.pop(); stk.push(pre / num); break; } // 更新符号为当前符号,数字清零 sign = c; num = 0; } } 当遇到空格时,只需要控制空格不进入 if 条件: if ((!isdigit(c) \u0026\u0026 c != ' ') || i == s.size() - 1) { ... } ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/:2:3","tags":["labuladong","数据结构"],"title":"【数据结构设计】实现一个计算器","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/"},{"categories":["labuladong的算法秘籍"],"content":"2.4 处理括号 💥 括号具有递归性质。 我们只需在遍历时,遇到 ( 开始递归,遇到 ) 结束递归。 def calculate(s: str) -\u003e int: def helper(s: List) -\u003e int: stack = [] sign = '+' num = 0 while len(s) \u003e 0: c = s.popleft() if c.isdigit(): num = 10 * num + int(c) # 遇到左括号开始递归计算 num if c == '(': num = helper(s) if (not c.isdigit() and c != ' ') or len(s) == 0: if sign == '+': stack.append(num) elif sign == '-': stack.append(-num) elif sign == '*': stack[-1] = stack[-1] * num elif sign == '/': # python 除法向 0 取整的写法 stack[-1] = int(stack[-1] / float(num)) num = 0 sign = c # 遇到右括号返回递归结果 if c == ')': break return sum(stack) return helper(collections.deque(s)) ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/:2:4","tags":["labuladong","数据结构"],"title":"【数据结构设计】实现一个计算器","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%AE%A1%E7%AE%97%E5%99%A8/"},{"categories":["C++"],"content":"1 简介 🟠 vector 容器实现的是一个动态数组,即可以进行元素的插入和删除,并动态调整所占用的内存空间,整个过程无需人工干预。 🟡 在尾部插入/删除元素时,时间复杂度为O(1);在头部或者中部插入/删除元素时,时间复杂度为O(n)。 🔵 使用需加上头文件:#include \u003cvector\u003e ","date":"2023-03-16","objectID":"/stlvector%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:1:0","tags":["C++","STL","vector"],"title":"【STL】vector容器用法","uri":"/stlvector%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"2 创建vector容器 1️⃣ 调用 vector 容器类的默认构造函数。(若默认指定了 std 命令空间,则 std:: 可省略) std::vector\u003cint\u003e v1; 2️⃣ 在创建 vector 容器的同时,进行初始化。 std::vector\u003cint\u003e v1 {2, 3, 5, 7, 11, 13, 17, 19}; 3️⃣ 在创建 vector 容器时,指定元素个数。 v1 容器开始时就有 20 个元素,它们的默认初始值都为 0。圆括号中的2个参数既可以是常量,也可以是变量。 std::vector\u003cint\u003e v1(20, 0); 4️⃣ 通过迭代器,取已建 vector 容器中指定区域内的键值对,创建并初始化新的 vector 容器。 int array[]={1,2,3}; std::vector\u003cint\u003ev1 (array, array+2); //v1 将保存{1,2} std::vector\u003cint\u003ev1 {1,2,3,4,5}; std::vector\u003cint\u003ev2 (std::begin(v1),std::begin(v1)+3); //v2保存{1,2,3} ","date":"2023-03-16","objectID":"/stlvector%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:2:0","tags":["C++","STL","vector"],"title":"【STL】vector容器用法","uri":"/stlvector%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"3 常用的成员方法 成员方法 功能 begin() 返回指向容器中第一个元素的迭代器。 end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。 size() 返回实际元素个数。 capacity() 返回当前容量。 empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。 reserve() 增加容器的容量。 front() 返回第一个元素的引用。 back() 返回最后一个元素的引用。 push_back() 在序列的尾部添加一个元素。 pop_back() 移出序列尾部的元素。 insert() 在指定的位置插入一个或多个元素。 erase() 移出一个元素或一段元素。 clear() 移出所有的元素,容器大小变为 0。 swap() 交换两个容器的所有元素。 emplace() 在指定的位置直接生成一个元素。 ","date":"2023-03-16","objectID":"/stlvector%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:3:0","tags":["C++","STL","vector"],"title":"【STL】vector容器用法","uri":"/stlvector%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"1 题目 力扣 380. O(1) 时间插入、删除和获取随机元素 实现RandomizedSet 类: RandomizedSet() 初始化 RandomizedSet 对象 bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false 。 bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false 。 int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。 你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1) 。 ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%B8%B8%E6%95%B0%E6%97%B6%E9%97%B4%E6%9F%A5%E6%89%BE%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0/:1:0","tags":["labuladong","数据结构"],"title":"【数据结构设计】常数时间查找数组元素","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%B8%B8%E6%95%B0%E6%97%B6%E9%97%B4%E6%9F%A5%E6%89%BE%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"2 解析 考虑题目的两个要求: 🔴 插入、删除、查询随机元素的时间复杂度必须都是 O(1)。 想到使用 STL 中的 map 结构。 🟡 getRandom() 必须等概率的返回随机元素。 那么底层必须用数组实现,且数组是紧凑的,这样就可以直接生成随机数作为数组索引。 🟢 综合考虑以上2个条件:在 O(1) 的时间删除数组中的某⼀个元素 val,可以先把这个元素交换到数组的尾部,然后再 pop 掉。而交换两个元素需要知道索引,故用哈希表存储每个元素及其索引。 代码实现: class RandomizedSet { public: // 存储元素的值 vector\u003cint\u003e ve; // 键是元素值,值是元素在ve中的索引 unordered_map\u003cint, int\u003e ma; RandomizedSet() { } bool insert(int val) { // 若 val 不存在,则插入并返回true if(ma.find(val) == ma.end()){ // 注意条件!别写反了 // 并记录 val 对应的索引值,注意添加键值对的写法 ma[val]=ve.size(); ve.push_back(val); return true; } return false; } bool remove(int val) { if(ma.find(val) != ma.end()){ // 将最后⼀个元素对应的索引修改为 ma[val] ma[ve.back()]=ma[val]; // 交换 val 和最后⼀个元素 swap(ve[ma[val]],ve.back()); // 在数组中删除元素 val ve.pop_back(); // 删除元素 val 对应的索引 ma.erase(val); return true; } return false; } int getRandom() { // 随机获取 nums 中的⼀个元素 return ve[rand() % ve.size()]; } }; ","date":"2023-03-16","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%B8%B8%E6%95%B0%E6%97%B6%E9%97%B4%E6%9F%A5%E6%89%BE%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0/:2:0","tags":["labuladong","数据结构"],"title":"【数据结构设计】常数时间查找数组元素","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1%E5%B8%B8%E6%95%B0%E6%97%B6%E9%97%B4%E6%9F%A5%E6%89%BE%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0/"},{"categories":["蓝桥杯刷题记录"],"content":"1 单词分析 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:1:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 请你帮助小蓝,给了一个单词后,帮助他找到出现最多的字母和这 个字母出现的次数。 输入描述 输入一行包含一个单词,单词只由小写英文字母组成。 对于所有的评测用例,输入的单词长度不超过 1000。 输出描述 输出两行,第一行包含一个英文字母,表示单词中出现得最多的字母是哪个。如果有多个字母出现的次数相等,输出字典序最小的那个。 第二行包含一个整数,表示出现得最多的那个字母在单词中出现的次数。 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:1:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:20min 🎯难度:💡 读取输入的单个字符:while((c=getchar())!='\\n') 两字符串相减就是整数,不用 (int) 强制类型转换 在 while() 的条件里用 ++,要检查是否会出错 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:1:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 #include \u003ciostream\u003eusing namespace std; int main(){ char c; // 每次读取进来的字符 int idx=0; // 记录个数最大的下标 int a[26]={0}; // 记录每个字符的个数 while((c=getchar())!='\\n'){ a[c-'a']++; } for(int i=1;i\u003c26;i++){ if(a[i]\u003ea[idx]) idx=i; } printf(\"%c\\n%d\", 'a'+idx, a[idx]); return 0; // 最好每次加上 } ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:1:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"2 成绩统计 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:2:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 小蓝给学生们组织了一场考试,卷面总分为 100 分,每个学生的得分都是一个 0 到 100 的整数。 如果得分至少是 60 分,则称为及格。如果得分至少为 85 分,则称为优秀。 请计算及格率和优秀率,用百分数表示,百分号前的部分四舍五入保留整 数。 输入描述 输入的第一行包含一个整数 n (1≤n≤$10^4$) ,表示考试人数。 接下来 n 行,每行包含一个 0 至 100 的整数,表示一个学生的得分。 输出描述 输出两行,每行一个百分数,分别表示及格率和优秀率。百分号前的部分 四舍五入保留整数。 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:2:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:20min 🎯难度:💡 注意 double d 四舍五入的写法:int i = d + 0.5 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:2:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 #include \u003ciostream\u003eusing namespace std; int main(){ int n,s,j=0,y=0; scanf(\"%d\",\u0026n); for(int i=0;i\u003cn;i++){ scanf(\"%d\",\u0026s); if(s\u003e=85) y++,j++; else if(s\u003e=60) j++; } printf(\"%d%\\n\",(int)((double)j/(double)n*100+0.5)); printf(\"%d%\\n\",(int)((double)y/(double)n*100+0.5)); } ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:2:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"3 门牌制作 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:3:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 小蓝要为一条街的住户制作门牌号。 这条街一共有 20202020 位住户,门牌号从 11 到 20202020 编号。 小蓝制作门牌的方法是先制作 00 到 99 这几个数字字符,最后根据需要将字符粘贴到门牌上,例如门牌 1017 需要依次粘贴字符 1、0、1、71、0、1、7,即需要 11 个字符 00,22 个字符 11,11 个字符 77。 请问要制作所有的 11 到 20202020 号门牌,总共需要多少个字符 22? ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:3:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:20min 🎯难度:💡 注意从获得一个数各个位上的数字的做法:先 ‘/’ 再 ‘%’ ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:3:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 #include \u003ciostream\u003eusing namespace std; int main(){ int s=0; for(int i=1;i\u003c=2020;i++){ if(i%10==2) s++; // 个位为2 if(i/10%10==2) s++; // 十位为2 if(i/100%10==2) s++; // 百位为2 if(i/1000==2) s++; // 千位为2 } printf(\"%d\",s); return 0; } ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:3:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"4 成绩分析 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:4:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 小蓝给学生们组织了一场考试,卷面总分为 100 分,每个学生的得分都是一个 0 到 100 的整数。 请计算这次考试的最高分、最低分和平均分。 输入描述 输入的第一行包含一个整数 n (1≤n≤$10^4$) ,表示考试人数。 接下来 n 行,每行包含一个 0 至 100 的整数,表示一个学生的得分。 输出描述 输出三行。 第一行包含一个整数,表示最高分。 第二行包含一个整数,表示最低分。 第三行包含一个实数,四舍五入保留正好两位小数,表示平均分。 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:4:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"答题总结 ⏰解题耗时:6min 🎯难度:💡 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:4:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 #include \u003ciostream\u003eusing namespace std; int main(){ int max=0,min=100,sum=0,n,s; scanf(\"%d\",\u0026n); for(int i=0;i\u003cn;i++){ scanf(\"%d\",\u0026s); if(s\u003emax) max=s; if(s\u003cmin) min=s; sum+=s; } double avg=(int)(sum*100.0/n+0.5); avg/=100; printf(\"%d\\n%d\\n%.2f\",max,min,avg); return 0; } ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:4:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"5 排序 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:5:0","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"题目 小蓝发现,如果对一个字符串中的字符排序,只允许交换相邻的两个字符, 则在所有可能的排序方案中,冒泡排序的总交换次数是最少的。 例如,对于字符串 lan 排序,只需要 11 次交换。对于字符串 qiao 排序,总共需要 44 次交换。 小蓝找到了很多字符串试图排序,他恰巧碰到一个字符串,需要 100 次交 换,可是他忘了把这个字符串记下来,现在找不到了。 请帮助小蓝找一个只包含小写英文字母且没有字母重复出现的字符串,对 该串的字符排序,正好需要 100 次交换。如果可能找到多个,请告诉小蓝最短的那个。如果最短的仍然有多个,请告诉小蓝字典序最小的那个。 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:5:1","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"答题情况 ⏰解题耗时:30min 🎯难度:💡💡 需要熟练掌握【冒泡排序】 ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:5:2","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["蓝桥杯刷题记录"],"content":"解析 首先需要回顾一下冒泡排序的基本内容。 🎨 冒泡排序流程 ① 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 ② 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。 ③ 针对所有的元素重复以上的步骤,除了最后已经选出的元素(有序)。 💦 冒泡排序代码 void bubble_sort(int arr[], int len) { int i, j; for (i = 0; i \u003c len - 1; i++) for (j = 0; j \u003c len - 1 - i; j++) if (arr[j] \u003e arr[j + 1]) swap(arr[j], arr[j + 1]); } 当拥有 n 个字符的字符串完全倒序时,交换次数最多,需要交换 n(n-1)/2 次。 而题目中需要交换 100 次,因此字符串长度至少为15。此时 15*14/2 = 105,最多交换 105 次,字符串为 “onmlkjihgfedcba” 。而结果字符串的字典顺序要小,因此把 j 提到最前面,交换次数减为 100,且此时字典顺序最小。 最终答案代码: #include \u003ciostream\u003eusing namespace std; int main() { // 请在此输入您的代码 cout\u003c\u003c\"jonmlkihgfedcba\"\u003c\u003cendl; return 0; } ","date":"2023-03-16","objectID":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/:5:3","tags":["蓝桥杯","C++"],"title":"【蓝桥杯】2020题解(上)","uri":"/%E8%93%9D%E6%A1%A5%E6%9D%AF2020%E4%B8%8A/"},{"categories":["CSP刷题记录"],"content":"1 // 需要用 CPP11 或 CPP14 // #include\u003cbits/stdc++.h\u003e #include \u003ciostream\u003eusing namespace std; int main(){ // 输入 n,m int n,m; scanf(\"%d %d\",\u0026n,\u0026m); // 输入数列 int a[n+1]={0}; for(int i=1;i\u003c=n;i++){ scanf(\"%d\",\u0026a[i]); } // 计算 int sum=0; int t=0; for(int i=1;i\u003cm;i++){ if(i\u003e=a[n]){ sum=sum+(m-i)*n; break; } for(int j=t+1;j\u003c=n;j++){ if(a[j]\u003ei){ t=j-1; sum=sum+t; break; } } } printf(\"%d\", sum); } ","date":"2023-03-15","objectID":"/csp202112/:1:0","tags":["CSP","C++"],"title":"【CSP】202112题解","uri":"/csp202112/"},{"categories":["CSP刷题记录"],"content":"2 错误的: ","date":"2023-03-15","objectID":"/csp202112/:2:0","tags":["CSP","C++"],"title":"【CSP】202112题解","uri":"/csp202112/"},{"categories":["CSP刷题记录"],"content":"3 #include \u003cbits/stdc++.h\u003eusing namespace std; long long w,s,f; string x; vector\u003clong long\u003ev; int main(){ cin\u003e\u003ew\u003e\u003es\u003e\u003ex; // 输入所有:w 每行能容纳的码字数,s 校验级别,x 字符串 long long tp=0; // 记录上一个字符的类型:0 大写字母,1 小写字母,2 数字 for(long long i=0;i\u003cx.length();i++){ if(x[i]\u003e='A'\u0026\u0026x[i]\u003c='Z'){ // 这个是大写字母 if(tp==1){ // 小→大:28 28 v.push_back(28); v.push_back(28); } else if(tp==2){ // 数→大:28 v.push_back(28); } tp=0; // 更新上一个字符类型为 0 v.push_back(x[i]-'A'); // 加入字符 } else if(x[i]\u003e='a'\u0026\u0026x[i]\u003c='z'){ // 这个是小写字母 if(tp==0||tp==2)v.push_back(27); // 大→小、数→小:27 v.push_back(x[i]-'a'); // 加入字符 tp=1; // 更新上一个字符类型为 1 } else { // 这个是数字 if(tp!=2)v.push_back(28); // 大→数、小→数:28 tp=2 // 更新上一个字符类型为 2 v.push_back(x[i]-'0'); // 加入字符 } } if(v.size()%2){ // 如果数字有奇数个,就补齐 v.push_back(29); } vector\u003clong long\u003ev2; // 存储码字 for(long long i=0;i\u003cv.size();i+=2){ // 计算码字 v2.push_back(v[i]*30+v[i+1]); } long long len=v2.size()+1; // 数据区码字个数=1(码字长度)+数据码字个数 if(s==-1){ while(len%w){ // 用 900 补齐 v2.push_back(900); ++len; } cout\u003c\u003clen\u003c\u003cendl; // 第一个是码字的数量 for(auto x:v2)cout\u003c\u003cx\u003c\u003cendl; return 0; } while((len+(1\u003c\u003c(s+1)))%w){ // 用 1\u003c\u003c(s+1) 计算校验码字的数目 ++len; v2.push_back(900); } vector\u003clong long\u003ev3; // v3 存储 dn 的系数,按高位到低位 v3.push_back(len); for(auto X:v2){ v3.push_back(X); } for(auto x:v3)printf(\"%d\\n\",x); // 输出码字长度+数据码字个数 vector\u003clong long\u003ev4; // 存储 g(x) 的系数,按高位到低位,最高 k 次 v4.push_back(1); v4.push_back(-3); long long mul=-3; for(long long i=2;i\u003c=(1\u003c\u003c(s+1));i++){ // 计算 g(x) 的系数 v4.push_back(0); mul=mul*3%929; // x 的 i 次方 for(long long j=v4.size()-1;j\u003e=1;j--) v4[j]+=v4[j-1]*mul%929,v4[j]%=929; } long long w4=1\u003c\u003c(s+1); // k for(long long i=1;i\u003c=w4;i++) v3.push_back(0); // 把 v3 变成 x 的 k+n-1 次方的系数 for(long long i=0;i\u003cv3.size();i++){ // i:相除时上的 x 的最高位 long long num=v3[i]; // x 的 i 次方的系数,除数 × 被除数最高位系数 for(long long j=i+1;j\u003c=i+w4;j++) // 相减时,最高位相同不用减,所以 j 从 i+1 开始,而 g(x) 除了最高位,最多还有 k 项 v3[j]-=num*v4[j-i]%929,v3[j]%=929; // 被除数最高位需要对齐的系数也在变化 if(i==v3.size()-1-w4){ f=i+1; break; } // } while(f\u003cv3.size()){ long long ans1=(-v3[f])%929; if(ans1\u003c0)ans1+=929; cout\u003c\u003cans1\u003c\u003cendl; ++f; } return 0; } #include \u003cbits/stdc++.h\u003eusing namespace std; int main(){ // 1. 输入数据 int w,s; // 每行能容纳的码字数、校验级别 string str; // 输入的非空字符串 vector\u003cint\u003e v1; // 最初的数字序列 cin\u003e\u003ew\u003e\u003es\u003e\u003estr; int k=(s==-1 ? 0 : (1\u003c\u003c(s+1))); // 2. 产生数字序列 int flag=-1; // 表示一个数属于哪一类:0 大写,1 小写,2 数字 for(int i=0;i\u003cstr.length();i++){ if(str[i]\u003e='A' \u0026\u0026 str[i]\u003c='Z'){ // 大写 if(flag==1){ // 前一个是小写 v1.push_back(28); v1.push_back(28); } else if(flag==2) // 前一个是数字 v1.push_back(28); v1.push_back(str[i]-'A'); flag=0; } else if(str[i]\u003e='a' \u0026\u0026 str[i]\u003c='z'){ // 小写 if(flag==0 || flag==2) // 前一个是大写或数字 v1.push_back(27); v1.push_back(str[i]-'a'); flag=1; } else if(str[i]\u003e='0' \u0026\u0026 str[i]\u003c='9'){ // 数字 if(flag==0 || flag==1) // 前一个是大写或数字 v1.push_back(28); v1.push_back(str[i]-'0'); flag=2; } } // 3. 计算有效数据码字 if(v1.size()%2) // 只有奇数个字 v1.push_back(29); vector\u003clong long\u003e v2; // 存储有效数据的码字 v2.push_back(0); // 第一个放数据码字的总个数,先占个坑位 for(int i=0;i\u003cv1.size();i+=2){ // 加入有效数据 v2.push_back(30*v1[i]+v1[i+1]); } while((v2.size()+k)%w) // 填充数据 v2.push_back(900); v2[0]=v2.size(); // 更新数据码字的总个数 // 4. 输出 数据码字 v2,s=-1 时,直接退出 for(int i=0;i\u003cv2.size();i++) cout\u003c\u003cv2[i]\u003c\u003cendl; if(s==-1) return 0; // 5. 计算 g(x) 中每项的系数 vector\u003clong long\u003e v3; // v3:存储 x 的系数,由高次到低次 v3.push_back(1); v3.push_back(-3); long long t=-3; for(int i=2;i\u003c=k;i++){ t=(t*3)%929; v3.push_back(0); for(int j=v3.size()-1;j\u003e=1;j--) v3[j]=(v3[j]+t*v3[j-1])%929; } // 6. 计算 d(x) 中每项的系数,存储在 v2 中 for(int i=0;i\u003ck;i++) v2.push_back(0); // 7. 计算余数 r(x) int a; for(int i=0;i\u003cv2.size();i++){ for(int j=i+1;j\u003c=i+k;j++) v2[j]=(v2[j]-v2[i]*v3[j-i])%929; if(i==v2.size()-1-k){ a=i+1; break; } } // 8. 输出最终结果 while(a\u003cv2.size()){ if((-v2[a])%929\u003c0) cout\u003c\u003c(-v2[a])%929+929\u003c\u003cendl; else cout\u003c\u003c(-v2[a])%929\u003c\u003cendl; a++; } return 0; } ","date":"2023-03-15","objectID":"/csp202112/:3:0","tags":["CSP","C++"],"title":"【CSP】202112题解","uri":"/csp202112/"},{"categories":["CSP刷题记录"],"content":"1 错误的: #include \u003ciostream\u003e#include \u003ccmath\u003e// #define _USE_MATH_DEFINES using namespace std; int main() { // 输入变量个数 n,语句个数 k int n,k; scanf(\"%d\",\u0026n); scanf(\"%d\",\u0026k); // 输入语句 int x[n+1]={0},y[n+1]={0},a[n+1]={0}; for(int i=1;i\u003c=k;i++){ scanf(\"%d\",\u0026x[i]); scanf(\"%d\",\u0026y[i]); } for(int i=1;i\u003c=k;i++){ if(a[x[i]]==0){ a[x[i]]=i; } } // 记录不符合的语句个数 int cnt=0; for(int i=1;i\u003c=k;i++){ if(y[i]!=0 \u0026\u0026 i\u003c=a[y[i]]){ cnt++; } } printf(\"%d\",cnt); } 正确的: // 需要用 CPP11 或 CPP14 // #include\u003cbits/stdc++.h\u003e #include \u003ciostream\u003eusing namespace std; int main(){ // 输入变量数量 n,赋值语句 k int n,k; scanf(\"%d %d\",\u0026n,\u0026k); // 用 cin 可能超时,改为 scanf // 输入 k 行语句 x[i] y[i] int x[k],y[k]; for(int i=0;i\u003ck;i++){ scanf(\"%d %d\",\u0026x[i],\u0026y[i]); } // 判断有无不符合赋值规则的 int cnt=0; for(int i=0;i\u003ck;i++){ int flag=1; if(y[i]==0) continue; for(int j=0;j\u003ci;j++){ if(x[j]==y[i]){ flag=0; break; } } cnt += flag; } cout\u003c\u003ccnt; } ","date":"2023-03-14","objectID":"/csp202203/:1:0","tags":["CSP","C++"],"title":"【CSP】202203题解","uri":"/csp202203/"},{"categories":["CSP刷题记录"],"content":"2 // 需要用 CPP11 或 CPP14 // #include\u003cbits/stdc++.h\u003e #include \u003ciostream\u003eusing namespace std; int main(){ // 输入计划数 n,查询数 m,等核酸天数 k int n,m,k; scanf(\"%d %d %d\",\u0026n,\u0026m,\u0026k); // 输入 t[i] c[i] int t,c; // 错误原因:1. 数组开的不够大 2. 数组没有初始化 int r[200005]={0}; for(int i=0;i\u003cn;i++){ scanf(\"%d %d\",\u0026t,\u0026c); if(t-k\u003c1) continue; int temp=max(1,t-k-c+1); for(int j=temp;j\u003c=t-k;j++){ r[j]++; } } // 输入 m 个 q[i] int q; for(int i=0;i\u003cm;i++){ int cnt=0; scanf(\"%d\",\u0026q); printf(\"%d\\n\",r[q]); } } ","date":"2023-03-14","objectID":"/csp202203/:2:0","tags":["CSP","C++"],"title":"【CSP】202203题解","uri":"/csp202203/"},{"categories":["CSP刷题记录"],"content":"1 #include \u003ciostream\u003e#include \u003ccmath\u003e// #define _USE_MATH_DEFINES using namespace std; int main() { // 输入 n,a,计算总和 sum int n,sum=0; scanf(\"%d\",\u0026n); int a[n]; for(int i=0;i\u003cn;i++){ scanf(\"%d\",\u0026a[i]); sum+=a[i]; } // 计算平均数 x double x=(double)sum/n; // 计算方差 d double d=0; for(int i=0;i\u003cn;i++){ d=d+(a[i]-x)*(a[i]-x); } d=d/n; // 计算 f double f[n]; for(int i=0;i\u003cn;i++){ f[i]=(a[i]-x)/pow(d,0.5); printf(\"%.16f\\n\",f[i]); } } ","date":"2023-03-14","objectID":"/csp202206/:1:0","tags":["CSP","C++"],"title":"【CSP】202206题解","uri":"/csp202206/"},{"categories":["CSP刷题记录"],"content":"2 #include \u003ciostream\u003e#include \u003ccmath\u003e using namespace std; int main() { // 输入变量 int n,l,s; scanf(\"%d %d %d\",\u0026n,\u0026l,\u0026s); int b[s+1][s+1]; // 输入树的位置 int x[n],y[n]; for(int i=0;i\u003cn;i++){ scanf(\"%d %d\",\u0026x[i],\u0026y[i]); } // 输入矩阵 S for(int i=s;i\u003e=0;i--){ for(int j=0;j\u003c=s;j++){ scanf(\"%d\",\u0026b[i][j]); } } int cnt=0; for(int i=0;i\u003cn;i++){ // 考虑树为起点构成的数组不能超过矩阵 L if(x[i]+s\u003el || y[i]+s\u003el){ continue; // 救命!这里用 continue,不是 break } // 构建新的矩阵 a int a[60][60]={0}; int flag=1; for(int j=0;j\u003cn;j++){ if(x[j]\u003e=x[i] \u0026\u0026 x[j]\u003c=x[i]+s \u0026\u0026 y[j]\u003e=y[i] \u0026\u0026 y[j]\u003c=y[i]+s){ a[x[j]-x[i]][y[j]-y[i]]=1; } } for(int j=0;j\u003c=s;j++){ for(int k=0;k\u003c=s;k++){ if(a[j][k]!=b[j][k]){ flag=0; break; } } if(flag==0) break; } cnt += flag; } printf(\"%d\",cnt); } ","date":"2023-03-14","objectID":"/csp202206/:2:0","tags":["CSP","C++"],"title":"【CSP】202206题解","uri":"/csp202206/"},{"categories":["CSP刷题记录"],"content":"3 #include\u003cbits/stdc++.h\u003eusing namespace std; class role{ public: set\u003cstring\u003e opl; set\u003cstring\u003e opt; set\u003cstring\u003e opn; }; class group{ public: set\u003cstring\u003e rol; // 关联内的用户组 }; map\u003cstring,role\u003e mpr; // 角色:键为角色名,值为角色的操作名、资源种类、资源名 map\u003cstring,group\u003e mpg; // 关联:键为角色名,值为一组用户组名 class user{ public: set\u003cstring\u003e rol; // 用户所关联的角色 set\u003cstring\u003e grp; // 用户所在的用户组 // 查看操作、资源种类、资源名是否能被用户使用 bool check(string opl,string opt,string opn){ // 遍历用户所关联的角色 for(auto it:rol) if(mpr[it].opl.count(\"*\") || mpr[it].opl.count(opl)) // 操作是否符合 if(mpr[it].opt.count(\"*\") || mpr[it].opt.count(opt)) // 资源种类是否符合 if(mpr[it].opn.empty() || mpr[it].opn.count(opn)) // 资源名是否符合 return true; // 遍历用户所在的用户组所关联的角色 for(auto it:grp) if(mpg.count(it)) // 已关联的用户是否包含用户所在的用户组 for(auto it1:mpg[it].rol) // 遍历用户组所关联的角色 if(mpr[it1].opl.count(\"*\") || mpr[it1].opl.count(opl)) if(mpr[it1].opt.count(\"*\") || mpr[it1].opt.count(opt)) if(mpr[it1].opn.empty() || mpr[it1].opn.count(opn)) return true; return false; } }; map\u003cstring,user\u003e mpu; // 关联:键为角色名,值为用户名 signed main(){ //提高cin,cout的速度 ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); int n,m,q,nv,no,nn,k; string name,rname,uname,x,y,z,ch; cin\u003e\u003en\u003e\u003em\u003e\u003eq; // 输入角色数 n,角色关联数 m,操作数 q for(int i=0;i\u003cn;i++){ cin\u003e\u003ename\u003e\u003env; // 可以直接用 cin,不用标准输入 while(nv--) cin\u003e\u003ex,mpr[name].opl.emplace(x); // 输入操作名 cin\u003e\u003eno; while(no--) cin\u003e\u003ex,mpr[name].opt.emplace(x); // 输入资源种类 cin\u003e\u003enn; while(nn--) cin\u003e\u003ex,mpr[name].opn.emplace(x); // 输入资源名 } for(int i=0;i\u003cm;i++){ cin\u003e\u003ername\u003e\u003ek; for(int j=0;j\u003ck;j++){ cin\u003e\u003ech\u003e\u003ename; if(ch == \"g\") mpg[name].rol.emplace(rname); // 在【用户组】中加入关联:角色名+用户组名 else mpu[name].rol.emplace(rname); // 在【用户】中加入关联:角色名+用户名 } } for(int i=0;i\u003cq;i++){ cin\u003e\u003euname\u003e\u003ek; for(int j=0;j\u003ck;j++) cin\u003e\u003ename,mpu[uname].grp.emplace(name); // 输入用户所在的用户组 cin\u003e\u003ex\u003e\u003ey\u003e\u003ez; cout\u003c\u003cmpu[uname].check(x,y,z)\u003c\u003cendl; mpu[uname].grp.clear(); // 每次要清空用户所在的用户组,role 不用清空!!! } } ","date":"2023-03-14","objectID":"/csp202206/:3:0","tags":["CSP","C++"],"title":"【CSP】202206题解","uri":"/csp202206/"},{"categories":["CSP刷题记录"],"content":"1 如此编码 🔗 题目:如此编码 本来看到题目很迷茫来着,想着第一题怎么就那么难,后来发现题目最后有提示,看完之后醍醐灌顶, 【取模】 确实是很妙的思路,可以积累下来。 代码忘存了QAQ。 ","date":"2023-03-14","objectID":"/csp202209/:1:0","tags":["CSP","C++"],"title":"【CSP】202209题解","uri":"/csp202209/"},{"categories":["CSP刷题记录"],"content":"2 何以包邮 🔗 题目:何以包邮 🔴 【动态规划】 设 sum 为所有参考书价格总和,题目可以理解为在 sum-x 价格内,最大化被删除的书价格总和,这样就可以把这个问题看作经典的 01背包问题 。(要学会转化💥💥💥) #include \u003ciostream\u003e#include \u003ccmath\u003e using namespace std; int main() { // 输入书本数量 n,包邮条件 x int n,x; scanf(\"%d\",\u0026n); scanf(\"%d\",\u0026x); // 输入书本价格,计算总和 sum int a[n]; int sum=0; for(int i=0;i\u003cn;i++){ scanf(\"%d\",\u0026a[i]); sum+=a[i]; } // 动态规划,找前 i 本书的价格和最大,不超过 sum-x int d[n][sum-x+1]={0}; for(int j=0;j\u003csum-x+1;j++){ d[0][j]=a[0]\u003ej ? 0 : a[0]; } for(int i=1;i\u003cn;i++){ for(int j=0;j\u003csum-x+1;j++){ if(a[i] \u003c=j){ // 注意这个条件 d[i][j]=max(d[i-1][j-a[i]]+a[i],d[i-1][j]); } else d[i][j]=d[i-1][j]; } } printf(\"%d\",sum-d[n-1][sum-x]); } ","date":"2023-03-14","objectID":"/csp202209/:2:0","tags":["CSP","C++"],"title":"【CSP】202209题解","uri":"/csp202209/"},{"categories":["CSP刷题记录"],"content":"3 防疫大数据 🔗 题目:防疫大数据 🟠🟠🟠 这道题读题有点绕,最重要的是选择合适的数据结构,然后一步一步分析。重要结构如下: ✅ 首先考虑漫游数据 \u003cd,u,r\u003e,我们用结构 my 来存储,并构造一个 my 类型的容器存储风险用户: vector\u003cmy\u003e u1 。这里之所以把 \u003cd,u,r\u003e 都存下来而不是只存 u,是为了方便后续判断用户某时某地的访问数据是否还有风险。 ✅ 考虑风险地区。由于是否有风险是由日期、地区共同决定的, 我们使用 map\u003cpair\u003cint,int\u003e,bool\u003e mp 来表示某时某地是否有风险。 pair\u003cint,int\u003e 类型变量为键,存储地区、日期; bool 型变量为值,存储是否有风险。 ✅ 由于最终结果要按用户编号从小到大输出,且同一用户只能输出一次,所以我们 选用有自动排序、自动去重功能的 set 容器 。 🟡🟡🟡 解题步骤: ✅ 清除 mp 中日期超过 7 天的记录,提高效率。( 注意删除时要用 it1,直接 erase(iter) 会报错 ,虽然我还不知道为什么QAQ) ✅ 添加风险地区: mp[{p,j}]=1 ✅ 更新以前日期的漫游数据中的风险用户。因为日期更新了一天,所以先前存的风险用户可能已经不再有风险了,需要更新。注意题目要求是 【对所有的 D∈[j.d, i],地区 j.r 都在风险范围内】 ,中间有任意一天不属于风险用户也要丢掉, 所以不能只看 j.d 或 i 那天是否为风险用户 。(如果中间某天非风险,则说明原来那条漫游数据无风险,只是后面某天用户又去了同一个地点,那个地点刚好仍有风险。这里有点绕,很容易出错QAQ) ✅ 更新今天漫游数据中的风险用户。和上一步的方法类似。 ✅ 输出答案。取 u1 中的 u 加入答案 ans 中输出。之所以不直接输出 u1 中的 u ,是因为题目要求最终结果由小到大排列且无重复用户。 🔵🔵🔵 在步骤 3 中,我们新开了一个 vector\u003cmy\u003e u2 来存放符合要求的数据,并让 u1=u2,而不是在 u1 中直接删改,是因为可以避免频繁修改数组,提高效率 🟣🟣🟣 这种要求复杂、代码变量较多的题目,一定要自习检查变量是否用错,好几个 bug 都是因为容器索引的参数写错。 💢💢💢 #include \u003cbits/stdc++.h\u003eusing namespace std; struct my{ int d;int u;int r; // 一条漫游数据 }; vector\u003cmy\u003e u1; // 记录当天有风险的用户的漫游数据,因为不能重复存储,所以用vector map\u003cpair\u003cint,int\u003e,bool\u003e mp; // 记录有风险的地区。键:地区、日期,值:是否是风险地 int main() { int n,r,m,p,di,ui,ri; cin\u003e\u003en; // 输入天数 n for(int i=0;i\u003cn;i++){ cin\u003e\u003er\u003e\u003em; // 输入风险地数量、漫游数据数量 // 1. 清除 mp 中日期超过 7 天的记录 for(map\u003cpair\u003cint,int\u003e,bool\u003e::iterator iter = mp.begin();iter!=mp.end();){ // 注意这里要用 it1,直接 erase(iter) 会报错 map\u003cpair\u003cint,int\u003e,bool\u003e::iterator it1 = iter; iter++; // 每次新的一天,检查是否有 7 天外的风险地区数据,有就删掉 if(i - ((it1-\u003efirst).second) \u003e= 7) mp.erase(it1); } // 2. 添加风险地区 for(int j=0;j\u003cr;j++){ cin\u003e\u003ep; for(int j=i;j\u003ci+7;j++) mp[{p,j}]=1; // 这里的参数原来写错了 } // 3. 更新以前日期的漫游数据中的风险用户 vector\u003cmy\u003e u2; for(auto j:u1){ // 这里原来写错了,是 u1 而不是 u2 if(i-j.d \u003e= 7) // 已经是 7 天前的数据了 continue; int flag=1; for(int k=j.d;k\u003c=i;k++){ if(!mp[{j.r, k}]){ // 不满足:对所有的 D∈[j.d, i],地区 j.r 都在风险范围内 flag=0;break; } } if(flag) // 只插入从漫游时间到现在,都满足风险的数据 u2.push_back(j); } u1.clear(); // 更新 u1 u1=u2; // 4. 更新今天漫游数据中的风险用户 for(int j=0;j\u003cm;j++){ cin\u003e\u003edi\u003e\u003eui\u003e\u003eri; if(i-di\u003e=7) continue; // 7 天外的数据不用考虑 int flag=1; for(int k=di;k\u003c=i;k++){ if(!mp[{ri, k}]){ // 不满足:对所有的 D∈[j.d, i],地区 j.r 都在风险范围内 flag=0;break; } } if(flag) u1.push_back({di,ui,ri}); } // 5. 输出答案 set\u003cint\u003e ans; for(auto j:u1) ans.insert(j.u); // 只取用户,同时满足用户按顺序输出 cout\u003c\u003c i \u003c\u003c ' '; for(auto j:ans) cout\u003c\u003c j \u003c\u003c' '; cout\u003c\u003c '\\n'; } return 0; } ","date":"2023-03-14","objectID":"/csp202209/:3:0","tags":["CSP","C++"],"title":"【CSP】202209题解","uri":"/csp202209/"},{"categories":["CSP刷题记录"],"content":"1 现值计算 🔗 题目:现值计算 🔴 注意 s=s/(1+i) + a[j] 是从后往前更新的,一开始写成从 a[0] 到 a[n] 出了 bug。 #include \u003ciostream\u003eusing namespace std; int main() { // 输入年数n,年利率i int n; double i; scanf(\"%d\", \u0026n); scanf(\"%lf\",\u0026i); double s=0; // 保存最后结果 int a[n+1]; // 每年收益 for(int j=0;j\u003cn+1;j++){ scanf(\"%d\", \u0026a[j]); } for(int j=n;j\u003e=0;j--){ s=s/(1+i) + a[j]; } printf(\"%f\", s); } ","date":"2023-03-14","objectID":"/csp202212/:1:0","tags":["CSP","C++"],"title":"【CSP】202212题解","uri":"/csp202212/"},{"categories":["CSP刷题记录"],"content":"2 训练计划 🔗 题目:训练计划 #include \u003ciostream\u003eusing namespace std; int main() { // 输入天数n 科目数m int n,m; scanf(\"%d\",\u0026n); scanf(\"%d\",\u0026m); // 输入依赖科目p[i] int p[m+1]={0},t[m+1]={0}; for(int i=1;i\u003c=m;i++){ scanf(\"%d\",\u0026p[i]); } // 输入科目所需天数t[i] for(int i=1;i\u003c=m;i++){ scanf(\"%d\",\u0026t[i]); } // 计算最早开始天数 int flag=0; int beg[m+1]={0},end[m+1]={0}; for(int i=1;i\u003c=m;i++){ if(p[i]==0){ beg[i]=1; end[i]=t[i]; } else{ beg[i]=end[p[i]]+1; end[i]=beg[i]+t[i]-1; } if(end[i]\u003en){ flag=1; } } for(int i=1;i\u003c=m;i++) printf(\"%d \",beg[i]); // 计算最晚开始天数 if(flag==1) { return 0; } else{ // 存储当前科目被哪个科目依赖 int last[m+1]={0}; for(int i=m;i\u003e0;i--){ last[i]=n+1; // 记录依赖当前科目的科目中,最早的【最晚开始时间】 for(int j=i+1;j\u003c=m;j++){ if(p[j]==i){ last[i]=min(last[i],last[j]); } } last[i]=last[i]-t[i]; } printf(\"\\n\"); for(int i=1;i\u003c=m;i++) printf(\"%d \",last[i]); } } ","date":"2023-03-14","objectID":"/csp202212/:2:0","tags":["CSP","C++"],"title":"【CSP】202212题解","uri":"/csp202212/"},{"categories":["CSP刷题记录"],"content":"3 JPEG解码 🔗 题目:JPEG解码 🟠 这种题目难度不是很大,但题目很长,需要把要求一步步分解,耐心计算即可。 🟡 蛇形矩阵填充,由于本题矩阵大小固定,可以 用矩阵 idx[8][8] 来存储对应位置填充的元素下标 ,就可以很方便的完成存储。 #include \u003ciostream\u003e#include \u003ccmath\u003e// #define _USE_MATH_DEFINES using namespace std; int main() { // 输入量化矩阵 Q int Q[8][8]; for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ scanf(\"%d\",\u0026Q[i][j]); } } // 输入扫描个数 n,任务 T int n,T; scanf(\"%d\",\u0026n); scanf(\"%d\",\u0026T); // 输入一组扫描数据 int d[64]={0}; for(int i=0;i\u003cn;i++){ scanf(\"%d\",\u0026d[i]); } // 将扫描数据放入填充矩阵 M 中 int M[8][8]={0}; int idx[8][8] = { {0, 1, 5, 6, 14, 15, 27, 28}, {2, 4, 7, 13, 16, 26, 29, 42}, {3, 8, 12, 17, 25, 30, 41, 43}, {9, 11, 18, 24, 31, 40, 44, 53}, {10, 19, 23, 32, 39, 45, 52, 54}, {20, 22, 33, 38, 46, 51, 55, 60}, {21, 34, 37, 47, 50, 56, 59, 61}, {35, 36, 48, 49, 57, 58, 62, 63} }; for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ M[i][j]=d[idx[i][j]]; } } // 输出 M 矩阵 if(T==0){ for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ printf(\"%d \", M[i][j]); } printf(\"\\n\"); } return 0; } // 计算与量化矩阵 Q 相乘后的矩阵 M for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ M[i][j]=M[i][j]*Q[i][j]; } } // 输出与量化矩阵 Q 相乘后的矩阵 M if(T==1){ for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ printf(\"%d \", M[i][j]); } printf(\"\\n\"); } return 0; } // 对 M 进行离散余弦逆变换 int MM[8][8]; for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ double s=0; for(int u=0;u\u003c8;u++){ for(int v=0;v\u003c8;v++){ double temp=cos(acos(-1)*u*(i+0.5)/8)*cos(acos(-1)*v*(j+0.5)/8)*M[u][v]; if(u==0) temp *= pow(0.5,0.5); if(v==0) temp *= pow(0.5,0.5); s+=temp; } } s=s/4; MM[i][j]=(int)(s+128.5); MM[i][j] = MM[i][j]\u003e255 ? 255 : MM[i][j]; MM[i][j] = MM[i][j]\u003c0 ? 0 : MM[i][j]; } } // 输出与量化矩阵 Q 相乘后的矩阵 M if(T==2){ for(int i=0;i\u003c8;i++){ for(int j=0;j\u003c8;j++){ printf(\"%d \", MM[i][j]); } printf(\"\\n\"); } return 0; } } ","date":"2023-03-14","objectID":"/csp202212/:3:0","tags":["CSP","C++"],"title":"【CSP】202212题解","uri":"/csp202212/"},{"categories":["C++"],"content":"1 简介 🟠 map 容器中键值对的键和值可以是任意数据类型,包括 C++ 基本数据类型(int、double 等)、使用结构体或类自定义的类型。 🟡 容器会自动根据各键值对的键的大小,按照既定的规则进行排序。比如 std::less\u003cT\u003e、std::greater\u003cT\u003e 规则。 🟢 键的值既不能重复也不能被修改。 🔵 使用需加上头文件:#include \u003cmap\u003e ","date":"2023-03-14","objectID":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:1:0","tags":["C++","STL","map"],"title":"【STL】map容器用法","uri":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"2 创建map容器 1️⃣ 调用 map 容器类的默认构造函数。(若默认指定了 std 命令空间,则 std:: 可省略) std::map\u003cstd::string, int\u003e map1; 2️⃣ 在创建 map 容器的同时,进行初始化。 std::map\u003cstd::string, int\u003e map1 { {\"语文\",90} , {\"数学\",100} }; 3️⃣ 利用先前已创建好的 map 容器和拷贝构造函数,再创建一个新的 map 容器。 std::map\u003cstd::string, int\u003e newMap(map1); 4️⃣ 通过迭代器,取已建 map 容器中指定区域内的键值对,创建并初始化新的 map 容器。 std::map\u003cstd::string, int\u003e map1 { {\"语文\",90} , {\"数学\",100} }; std::map\u003cstd::string, int\u003e newMap(++map1.begin(), map1.end()); ","date":"2023-03-14","objectID":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:2:0","tags":["C++","STL","map"],"title":"【STL】map容器用法","uri":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"3 常用的成员方法 成员方法 功能 begin() 返回指向容器中第一个键值对的双向迭代器。(是已排好序的第一个) end() 返回指向容器最后一个元素所在位置后一个位置的双向迭代器。(是已排好序的最后一个) find(key) 在 map 容器中查找键为 key 的键值对,如果成功找到,则返回指向该键值对的双向迭代器;否则返回和 end() 方法一样的迭代器。 empty() 若容器为空,则返回 true;否则 false。 size() 返回当前 map 容器中存有键值对的个数。 insert() 向 map 容器中插入键值对。 erase() 删除 map 容器指定位置、指定键(key)值或者指定区域内的键值对。 clear() 清空 map 容器中所有的键值对,即使 map 容器的 size() 为 0。 emplace() 在当前 map 容器中的指定位置处构造新键值对。其效果和插入键值对一样,但效率更高。 count(key) 在当前 map 容器中,查找键为 key 的键值对的个数并返回。注意,由于 map 容器中各键值对的键的值是唯一的,因此该函数的返回值最大为 1。 count(key) 在容器中查找以 key 键的键值对的个数。 ","date":"2023-03-14","objectID":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:3:0","tags":["C++","STL","map"],"title":"【STL】map容器用法","uri":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"4 其他容器 ","date":"2023-03-14","objectID":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:4:0","tags":["C++","STL","map"],"title":"【STL】map容器用法","uri":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"unordered_map 容器 🟠 该容器内部不会自行对存储的键值对进行排序,其余用法和 map 类似。 🟡 使用需加上头文件: #include \u003cunordered_map\u003e ","date":"2023-03-14","objectID":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:4:1","tags":["C++","STL","map"],"title":"【STL】map容器用法","uri":"/stlmap%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"1 简介 C++ 标准函数库中的 set 可以用来存储集合,set 里面的元素都是唯一的,不可以重复,可以新增或删除元素,但不可以修改元素的值。 🔴 头文件:#include \u003cset\u003e ","date":"2023-03-13","objectID":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:1:0","tags":["C++","STL","set"],"title":"【STL】set容器用法","uri":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"2 初始化 std::set 的初始化有三种方式:1️⃣ 以 insert() 函數新增元素 2️⃣ 直接在创建时以大括号初始化 set 内部的元素 3️⃣ 通过数组初始化。 // 第 1 种初始化方式 set\u003cint\u003e set1; set1.insert(1); set1.insert(2); set1.insert(3); // 第 2 种初始化方式 // 注意这里没有 '=' set\u003cint\u003e set2 {1,2,3}; // 第 3 种初始化方式 int num[] = {1,2,3}; set\u003cint\u003e set3(num, num+3); ","date":"2023-03-13","objectID":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:2:0","tags":["C++","STL","set"],"title":"【STL】set容器用法","uri":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"3 增/删元素 std::set 若要新增、刪除元素,可以使用 insert() 和 erase() 函数。 // 新增元素 set1.insert(1); // 删除元素 set1.erase(1); ","date":"2023-03-13","objectID":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:3:0","tags":["C++","STL","set"],"title":"【STL】set容器用法","uri":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"4 查询元素 查询 std::set 中是否包含特定的元素,可以使用 find() 函数,若成功找到指定的元素,就返回对应的 iterator,而如果没有找到,就返回 set::end。 // 在 set1 中寻找 6 这个元素 set\u003cint\u003e::iterator iter; iter = set1.find(6); // 如果找到,就返回正确的 iterator,否则返回 set1.end() if (iter != set1.end()) { cout \u003c\u003c \"Found: \" \u003c\u003c *iter \u003c\u003c endl; } ","date":"2023-03-13","objectID":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:4:0","tags":["C++","STL","set"],"title":"【STL】set容器用法","uri":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"5 列出所有元素 列出 std::set 中的所有元素有2种方式:1️⃣ 标准的 iterator 方式 2️⃣ 新的 for 遍历方法 🟠 想要拷贝元素:for(auto x:range) 🟡 想要修改元素:for(auto \u0026\u0026x:range) 🟢 想要只读元素:for(const auto\u0026 x:range) // 第 1 种列出所有元素的方法 set\u003cint\u003e::iterator iter; for(iter=set1.begin(); iter!=set1.end(); iter++){ cout \u003c\u003c *iter \u003c\u003c endl; // 注意 endl 不要写成 end } // 第 2 种列出所有元素的方法 for(const auto \u0026i : set1){ std::cout \u003c\u003c i \u003c\u003c endl; // 注意这里 i 前面没有 '*' } ","date":"2023-03-13","objectID":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:5:0","tags":["C++","STL","set"],"title":"【STL】set容器用法","uri":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["C++"],"content":"6 清空所有元素 1️⃣ 清空 std::set 中的所有元素: clear() 函数 2️⃣ 获取元素个数: size() 函数 3️⃣ 检查 std::set 是否是空的: empty() 函数。 // 清空所有元素 set1.clear(); // 获取元素个数: cout \u003c\u003c \"number of elements:\" \u003c\u003c set1.size() \u003c\u003c endl; // 检查 set 是否为空 if(set1.empty()){ cout \u003c\u003c \"set1 is empty.\" \u003c\u003c } ","date":"2023-03-13","objectID":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/:6:0","tags":["C++","STL","set"],"title":"【STL】set容器用法","uri":"/stlset%E5%AE%B9%E5%99%A8%E7%94%A8%E6%B3%95/"},{"categories":["OS"],"content":"1 绪论 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:1:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"2 OS的结构和硬件支持 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:2:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"3 操作系统的用户接口 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:3:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"4 进程及进程管理 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:4:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"5 资源分配与调度 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:5:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"6 处理机调度 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:6:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"7 主存管理 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:7:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"8 设备管理 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:8:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["OS"],"content":"9 文件系统 ","date":"2023-02-10","objectID":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:9:0","tags":["OS"],"title":"操作系统知识总结","uri":"/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"1 绪论 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:1:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"2 关系数据库 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:2:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"3 SQL语言 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:3:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"4 数据库安全性 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:4:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"5 数据库完整性 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:5:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"6 关系数据理论 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:6:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"7 数据库设计 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:7:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"8 关系数据库引擎基础 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:8:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"9 关系数据库查询优化 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:9:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"10 数据库恢复技术 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:10:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["数据库"],"content":"11 并发控制 ","date":"2023-02-06","objectID":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:11:0","tags":["数据库"],"title":"数据库原理知识总结","uri":"/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8E%9F%E7%90%86%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["labuladong的算法秘籍"],"content":"1 题目 力扣 146. LRU 缓存 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类: LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存 int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。 void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。 ","date":"2023-01-17","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/:1:0","tags":["labuladong","数据结构","LRU"],"title":"【数据结构设计】LRU算法","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"2 解析 ","date":"2023-01-17","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/:2:0","tags":["labuladong","数据结构","LRU"],"title":"【数据结构设计】LRU算法","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"2.1 LRU算法设计 由于 put 和 get 方法的时间复杂度为 O(1),我们可以总结出 cache 这个数据结构必要的条件: 🔴 cache 中的元素必须有时序,以区分最近使用的和久未使用的数据 🟡 能在 cache 中快速找某个 key 是否已存在并得到对应的 val 🟢 cache 要支持在任意位置快速插入和删除元素,便于访问某个 key 并将这个元素变为最近使用的 哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表 LinkedHashMap。 LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。 如图所示: 根据这个结构分析以上 3 个条件: 🟥 每次从链表尾部添加元素,则尾部就是最近使用的,头部就是最久未使用的 🟨 通过哈希表可以根据 key 快速获得 val 🟩 用哈希表将 key 快速映射到链表节点后,可以很方便的进行插入、删除操作 两个问题: ❓ 用单链表取代双链表行不行? ❓ 哈希表中存了 key,链表中为什么还要存 key 和 val?可以只存 val 吗? ","date":"2023-01-17","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/:2:1","tags":["labuladong","数据结构","LRU"],"title":"【数据结构设计】LRU算法","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"2.2 代码实现 首先写出双链表节点类型 Node: class Node { public int key, val; public Node next, prev; public Node(int k, int v) { this.key = k; this.val = v; } } 再用 Node 类型构建一个双链表: class DoubleList { // 头尾虚节点 private Node head, tail; // 链表元素数 private int size; public DoubleList() { // 初始化双向链表的数据 head = new Node(0, 0); tail = new Node(0, 0); head.next = tail; tail.prev = head; size = 0; } // 在链表尾部添加节点 x,时间 O(1) public void addLast(Node x) { x.prev = tail.prev; x.next = tail; tail.prev.next = x; tail.prev = x; size++; } // 删除链表中的 x 节点(x ⼀定存在) // 由于是双链表且给的是⽬标 Node 节点,时间 O(1) public void remove(Node x) { x.prev.next = x.next; x.next.prev = x.prev; size--; } // 删除链表中第⼀个节点,并返回该节点,时间 O(1) public Node removeFirst() { if (head.next == tail) return null; Node first = head.next; remove(first); return first; } // 返回链表⻓度,时间 O(1) public int size() { return size; } } ✅ 【为什么必须要用双向链表】 因为我们需要删除操作。删除⼀个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。 有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可,先搭出代码框架: class LRUCache { // key -\u003e Node(key, val) private HashMap\u003cInteger, Node\u003e map; // Node(k1, v1) \u003c-\u003e Node(k2, v2)... private DoubleList cache; // 最⼤容量 private int cap; public LRUCache(int capacity) { this.cap = capacity; map = new HashMap\u003c\u003e(); cache = new DoubleList(); } } 一些辅助函数的实现: /* 将某个 key 提升为最近使⽤的 */ private void makeRecently(int key) { Node x = map.get(key); // 先从链表中删除这个节点 cache.remove(x); // 重新插到队尾 cache.addLast(x); } /* 添加最近使⽤的元素 */ private void addRecently(int key, int val) { Node x = new Node(key, val); // 链表尾部就是最近使⽤的元素 cache.addLast(x); // 别忘了在 map 中添加 key 的映射 map.put(key, x); } /* 删除某⼀个 key */ private void deleteKey(int key) { Node x = map.get(key); // 从链表中删除 cache.remove(x); // 从 map 中删除 map.remove(key); } /* 删除最久未使⽤的元素 */ private void removeLeastRecently() { // 链表头部的第⼀个元素就是最久未使⽤的 Node deletedNode = cache.removeFirst(); // 同时别忘了从 map 中删除它的 key int deletedKey = deletedNode.key; map.remove(deletedKey); } ✅ 【为什么要在链表中同时存储 key 和 val,而不是只存储 val】 当缓存容量已满,我们不仅仅要删除最后⼀个 Node 节点,还要把 map 中映射到该节点的 key 也删除,而 key 只能由 Node 得到。如果 Node 结构中只存储 val,那么我们就无法得知 key 是什么,也就无法删除 map 中的键。 根据以上辅助函数,可以实现 put 和 get 函数: public int get(int key) { if (!map.containsKey(key)) { return -1; } // 将该数据提升为最近使⽤的 makeRecently(key); return map.get(key).val; } public void put(int key, int val) { if (map.containsKey(key)) { // 删除旧的数据 deleteKey(key); // 新插⼊的数据为最近使⽤的数据 addRecently(key, val); return; } if (cap == cache.size()) { // 删除最久未使⽤的元素 removeLeastRecently(); } // 添加为最近使⽤的元素 addRecently(key, val); } ","date":"2023-01-17","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/:2:2","tags":["labuladong","数据结构","LRU"],"title":"【数据结构设计】LRU算法","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"3 内置类型 LinkedHashMap 也可以用 Java 的内置类型 LinkedHashMap 来实现 LRU 算法,逻辑和之前完全⼀致: class LRUCache { int cap; LinkedHashMap\u003cInteger, Integer\u003e cache = new LinkedHashMap\u003c\u003e(); public LRUCache(int capacity) { this.cap = capacity; } public int get(int key) { if (!cache.containsKey(key)) { return -1; } // 将 key 变为最近使⽤ makeRecently(key); return cache.get(key); } public void put(int key, int val) { if (cache.containsKey(key)) { // 修改 key 的值 cache.put(key, val); // 将 key 变为最近使⽤ makeRecently(key); return; } if (cache.size() \u003e= this.cap) { // 链表头部就是最久未使⽤的 key int oldestKey = cache.keySet().iterator().next(); cache.remove(oldestKey); } // 将新的 key 添加链表尾部 cache.put(key, val); } private void makeRecently(int key) { int val = cache.get(key); // 删除 key,重新插⼊到队尾 cache.remove(key); cache.put(key, val); } } 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-17","objectID":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/:3:0","tags":["labuladong","数据结构","LRU"],"title":"【数据结构设计】LRU算法","uri":"/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1lru%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"【单调队列】主要是为了解决以下场景: 给你一个数组 window,已知其最值为 A,如果给 window 中添加⼀个数 B,那么比较一下 A 和 B 就可以立即算出新的最值;但如果要从 window 数组中减少一个数,就不能直接得到最值了,因为如果减少的这个数恰好是 A,就需要遍历 window 中的所有元素重新寻找新的最值。 可以使用 优先级队列 来动态寻找最值,通过创建一个大(小)顶堆,可以很快拿到最大(小)值。 但优先级队列无法满足标准队列结构【先进先出】的时间顺序,因为优先级队列底层利用二叉堆对元素进行动态排序,元素的出队顺序是元素的大小顺序,和入队的先后顺序完全没有关系。 而【单调队列】结构,既能够维护队列元素【先进先出】的时间顺序,又能够正确维护队列中所有元素的最值。 ","date":"2023-01-15","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/:0:0","tags":["labuladong","算法","队列和栈","滑动窗口"],"title":"【队列和栈】单调队列解决滑动窗口问题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"1 题目 力扣 239. 滑动窗口最大值 给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。 ","date":"2023-01-15","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/:1:0","tags":["labuladong","算法","队列和栈","滑动窗口"],"title":"【队列和栈】单调队列解决滑动窗口问题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2 解析 ","date":"2023-01-15","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/:2:0","tags":["labuladong","算法","队列和栈","滑动窗口"],"title":"【队列和栈】单调队列解决滑动窗口问题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.1 搭建解题框架 🟧 普通队列的标准 API: class Queue{ // enqueue 操作,在队尾加⼊元素 n void push(int n); // dequeue 操作,删除队头元素 void pop(); } 🟨 【单调队列】的 API: class MonotonicQueue{ // 在队尾添加元素 n void push(int n); // 返回当前队列中的最⼤值 int max(); // 队头元素如果是 n,删除它 void pop(int n); } 🟩 【滑动窗口】问题的解答框架 int[] maxSlidingWindow(int[] nums, int k) { MonotonicQueue window = new MonotonicQueue(); List\u003cInteger\u003e res = new ArrayList\u003c\u003e(); for (int i = 0; i \u003c nums.length; i++) { if (i \u003c k - 1) { //先把窗⼝的前 k - 1 填满 window.push(nums[i]); } else { // 窗⼝开始向前滑动 // 移⼊新元素 window.push(nums[i]); // 将当前窗⼝中的最⼤元素记⼊结果 res.add(window.max()); // 移出最后的元素 window.pop(nums[i - k + 1]); } } // 将 List 类型转化成 int[] 数组作为返回值 int[] arr = new int[res.size()]; for (int i = 0; i \u003c res.size(); i++) { arr[i] = res.get(i); } return arr; } ","date":"2023-01-15","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/:2:1","tags":["labuladong","算法","队列和栈","滑动窗口"],"title":"【队列和栈】单调队列解决滑动窗口问题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.2 实现单调队列数据结构 🟠 实现「单调队列」必须使用一种数据结构支持在头部和尾部进行插入和删 除,很明显双链表满足这个条件。 🟡 由于只输出每个窗口内的最大元素,所以实现 push 方法时依然在队尾添加元素,但要把前面比自己小的元素都删掉。 class MonotonicQueue { // 双链表,⽀持头部和尾部增删元素 // 维护其中的元素⾃尾部到头部单调递增 private LinkedList\u003cInteger\u003e maxq = new LinkedList\u003c\u003e(); // 在尾部添加⼀个元素 n,维护 maxq 的单调性质 public void push(int n) { // 将前⾯⼩于⾃⼰的元素都删除 while (!maxq.isEmpty() \u0026\u0026 maxq.getLast() \u003c n) { maxq.pollLast(); } maxq.addLast(n); } } 🟢 最终单调队列的元素会保持 单调递减 的顺序,因此 max 方法只用返回队首元素: public int max() { // 队头的元素肯定是最⼤的 return maxq.getFirst(); } 🔵 pop 方法在队头删除元素 n : public void pop(int n) { if (n == maxq.getFirst()) { maxq.pollFirst(); } } 完整代码: /* 单调队列的实现 */ class MonotonicQueue { LinkedList\u003cInteger\u003e maxq = new LinkedList\u003c\u003e(); public void push(int n) { // 将⼩于 n 的元素全部删除 while (!maxq.isEmpty() \u0026\u0026 maxq.getLast() \u003c n) { maxq.pollLast(); } // 然后将 n 加⼊尾部 maxq.addLast(n); } public int max() { return maxq.getFirst(); } public void pop(int n) { if (n == maxq.getFirst()) { maxq.pollFirst(); } } } /* 解题函数的实现 */ int[] maxSlidingWindow(int[] nums, int k) { MonotonicQueue window = new MonotonicQueue(); List\u003cInteger\u003e res = new ArrayList\u003c\u003e(); for (int i = 0; i \u003c nums.length; i++) { if (i \u003c k - 1) { //先填满窗⼝的前 k - 1 window.push(nums[i]); } else { // 窗⼝向前滑动,加⼊新数字 window.push(nums[i]); // 记录当前窗⼝的最⼤值 res.add(window.max()); // 移出旧数字 window.pop(nums[i - k + 1]); } } // 需要转成 int[] 数组再返回 int[] arr = new int[res.size()]; for (int i = 0; i \u003c res.size(); i++) { arr[i] = res.get(i); } return arr; } 🟣 nums 中的每个元素最多被 push 和 pop ⼀次,没有任何多余操作,所以整体的复杂度是 O(N)。空间复杂度就是窗口的大小 O(k)。 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-15","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/:2:2","tags":["labuladong","算法","队列和栈","滑动窗口"],"title":"【队列和栈】单调队列解决滑动窗口问题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97%E8%A7%A3%E5%86%B3%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E9%97%AE%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"1 单调栈模板 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:1:0","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"题目 输入一个数组 nums,请你返回⼀个等长的结果数组,结果数组中对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:1:1","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"解析 🔴 把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列。如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的下⼀个更大元素,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。 🟡 for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈,其实是正着出栈。 🟢 while 循环是把两个「高个子」元素之间的元素排除,因为他们的存在没有意义,前面挡着个「更高」的元素,所以他们不可能被作为后续进来的元素的下一个更大元素了。 🔵 这个算法的复杂度只有 O(n)。 总共有 n 个元素,每个元素都被 push 入栈了一次,而最多会被 pop一次,没有任何冗余操作。 图解: 代码实现: int[] nextGreaterElement(int[] nums) { int n = nums.length; // 存放答案的数组 int[] res = new int[n]; Stack\u003cInteger\u003e s = new Stack\u003c\u003e(); // 倒着往栈⾥放 for (int i = n - 1; i \u003e= 0; i--) { // 判定个⼦⾼矮 while (!s.isEmpty() \u0026\u0026 s.peek() \u003c= nums[i]) { // 矮个起开,反正也被挡着了。。。 s.pop(); } // nums[i] 身后的更⼤元素 res[i] = s.isEmpty() ? -1 : s.peek(); s.push(nums[i]); } return res; } ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:1:2","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"2 进阶——下一个更大元素 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:2:0","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 496. 下一个更大元素 I nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。 给你两个 没有重复元素 的数组 nums1 和 nums2 ,下标从 0 开始计数,其中nums1 是 nums2 的子集。 对于每个 0 \u003c= i \u003c nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j] 的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1 。 返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素 。 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:2:1","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"解析 🟠 和上一题类似,只需要先把 nums2 中每个元素的下一个更大元素算出来存到一个映射里,然后再让 nums1 中的元素去查表即可。 🟣 使用 HashMap 映射,可以提高代码效率。 代码实现: public int[] nextGreaterElement(int[] nums1, int[] nums2) { // 记录 nums2 中每个元素的下⼀个更⼤元素 int[] greater=nextGreaterElement(nums2); // 转化成映射:元素 x -\u003e x 的下⼀个最⼤元素 HashMap\u003cInteger, Integer\u003e greaterMap=new HashMap\u003c\u003e(); for(int i=0; i\u003cnums2.length; i++){ greaterMap.put(nums2[i], greater[i]); } // nums1 是 nums2 的⼦集,所以根据 greaterMap 可以得到结果 int[] res=new int[nums1.length]; for(int i=0; i\u003cnums1.length; i++){ res[i]=greaterMap.get(nums1[i]); } return res; } ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:2:2","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"3 进阶——下一个更大元素索引 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:3:0","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 739. 每日温度 给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:3:1","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"解析 🟡 这个问题本质上也是找下一个更大元素,只不过现在不是问你下一个更大元素的值是多少,而是问你当前元素距离下一个更大元素的索引距离而已。 🟤 把之前记录最大值的堆栈改为记录最大值索引的堆栈即可。 或是使用两个堆栈,一个记录索引,一个记录最大值,不过此时代码效率会降低。 代码实现: public int[] dailyTemperatures(int[] temperatures) { int n=temperatures.length; int[] res=new int[n]; // 这⾥放元素索引,⽽不是元素 Stack\u003cInteger\u003e index=new Stack\u003c\u003e(); /* 单调栈模板 */ for(int i=n-1; i\u003e=0; i--){ while(!index.isEmpty() \u0026\u0026 temperatures[index.peek()]\u003c=temperatures[i]){ index.pop(); } // 得到索引间距 res[i] = index.isEmpty() ? 0 : index.peek()-i; // 将索引⼊栈,⽽不是元素 index.push(i); } return res; } ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:3:2","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"4 进阶——处理环形数组 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:4:0","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 503. 下一个更大元素 II 给定一个循环数组 nums ( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。 数字 x 的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1 。 ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:4:1","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"解析 🟠 我们一般通过 % 运算符求模(余数),来模拟环形特效。 🟢 这道题的难点在于,比如输入是 [2,1,2,4,3],对于最后一个元素 3,如何找到元素 4 作为下一个更大元素。 🔵 对于这种需求,常用套路就是将数组长度翻倍。 🟣 我们可以不用构造翻倍的新数组,而是利用循环数组的技巧来模拟数组长度翻倍的效果。 图解: 代码实现: public int[] nextGreaterElements(int[] nums) { int n=nums.length; int[] res=new int[n]; Stack\u003cInteger\u003e s=new Stack\u003c\u003e(); // 数组⻓度加倍模拟环形数组 for(int i=2*n-1; i\u003e=0; i--){ // 索引 i 要求模,其他的和模板⼀样 while(!s.isEmpty() \u0026\u0026 s.peek()\u003c=nums[i%n]){ s.pop(); } res[i%n] = s.isEmpty() ? -1 : s.peek(); s.push(nums[i%n]); } return res; } 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-14","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/:4:2","tags":["labuladong","算法","队列和栈","下一个更大元素"],"title":"【队列和栈】单调栈解决下一个更大元素","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E5%8D%95%E8%B0%83%E6%A0%88%E8%A7%A3%E5%86%B3%E4%B8%8B%E4%B8%80%E4%B8%AA%E6%9B%B4%E5%A4%A7%E5%85%83%E7%B4%A0/"},{"categories":["labuladong的算法秘籍"],"content":"1.1 判断有效括号串 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:1:0","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 给定一个只包括 '(',')' 的字符串 s ,判断字符串是否有效。即每个右括号 ')' 的左边必须有⼀个左括号 '(' 和它匹配。 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:1:1","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 用一个变量 left 记录 '(' 相对于 ')' 的数量,遇到 '(' 就 +1,遇到 ')' 就 -1。如果最后 left==0,则括号串有效,否则无效。并且,如果中间出现 left 数量为负,则说明有 ')' 出现在 '(' 之前,也为无效。 代码实现: bool isValid(string str) { // 待匹配的左括号数量 int left = 0; for (int i = 0; i \u003c str.size(); i++) { if (s[i] == '(') { left++; } else { // 遇到右括号 left--; } // 右括号太多 if (left == -1) return false; } // 是否所有的左括号都被匹配了 return left == 0; } ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:1:2","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"1.2 判断有效括号串进阶 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:2:0","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 20. 有效的括号 给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 每个右括号都有一个对应的相同类型的左括号。 示例: 输入:s = \"()[]{}\" 输出:true ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:2:1","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 使用⼀个名为 left 的栈代替之前思路中的 left 变量,遇到左括号就入栈,遇到右括号就去栈 中寻找最近的左括号,看是否匹配。 代码实现: class Solution { public: bool isValid(string s) { stack\u003cchar\u003e left; for(char c:s){ // c是左括号 if(c=='(' || c=='{' || c=='[') left.push(c); else{ // c是右括号且栈中最近的左括号匹配 if(!left.empty() \u0026\u0026 leftOf(c)==left.top()) left.pop(); // 栈中最近的左括号不匹配 else return false; } } // 是否所有的左括号都被匹配了 return left.empty(); } char leftOf(char c){ if(c==')') return '('; else if(c=='}') return '{'; else return '['; } }; ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:2:2","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.1 平衡括号串 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:3:0","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 921. 使括号有效的最少添加 只有满足下面几点之一,括号字符串才是有效的: 它是一个空字符串,或者 它可以被写成 AB (A 与 B 连接), 其中 A 和 B 都是有效字符串,或者 它可以被写作 (A),其中 A 是有效字符串。 给定一个括号字符串 s ,在每一次操作中,你都可以在字符串的任何位置插入一个括号 例如,如果 s = \"()))\" ,你可以插入一个开始括号为 \"(()))\" 或结束括号为 \"())))\" 。 返回 为使结果字符串 s 有效而必须添加的最少括号数。 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:3:1","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 核心思路是以左括号为基准,通过维护对右括号的需求数 need,来计算最小的插入次数。 需要注意两个地方: 🟠 need == -1 need == -1 意味着右括号太多,需要及时判断插入左括号。注意后面多出的左括号不能抵消前面多出的右括号。 🟡 返回 res+need res 记录左括号的插入次数,need 记录右括号的需求。 代码实现: class Solution { public: int minAddToMakeValid(string s) { int res=0; // res记录插入次数 int need=0; // need记录右括号的需求量 for(char c:s){ if(c=='('){ need++; } else if(c==')'){ need--; if(need==-1){ // 需要插入一个左括号 need++; res++; } } } return res+need; } }; ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:3:2","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.2 平衡括号串进阶 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:4:0","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 1541. 平衡括号字符串的最少插入次数 给你一个括号字符串 s ,它只包含字符 '(' 和 ')' 。一个括号字符串被称为平衡的当它满足: 任何左括号 '(' 必须对应两个连续的右括号 '))' 。 左括号 '(' 必须在对应的连续两个右括号 '))' 之前。 比方说 \"())\", \"())(())))\" 和 \"(())())))\" 都是平衡的, \")()\", \"()))\" 和 \"(()))\" 都是不平衡的。 你可以在任意位置插入字符 ‘(’ 和 ‘)’ 使字符串平衡。 请你返回让 s 平衡的最少插入次数。 ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:4:1","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 🟧 首先,按照上面的思路维护 res 和 need 变量 🟨 need == -1 当 need == -1 时,说明遇到一个多余的右括号,则需要插入一个左括号,对右括号的需求变为 1。即 res++; need = 1; 🟩 遇到左括号 对右括号的需求量 +2。若此时 need 为奇数,则需要插入一个右括号,使 need 变为偶数。即 res++; need--; 代码实现: public: int minInsertions(string s) { int res=0; // 记录插入的括号数量 int need=0; // 记录右括号的需求量 for(char c:s){ if(c=='('){ need+=2; // 一个左括号对应两个右括号 if(need%2 == 1){ res++; // 插入一个右括号 need--; // 对右括号的需求减一 } } else{ need--; if(need==-1){ // 右括号多了 need=1; // 还需要再来一个右括号 res++; // 插入一个左括号 } } } return res+need; } 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-12","objectID":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/:4:2","tags":["labuladong","算法","队列和栈","括号题"],"title":"【队列和栈】详解3道括号题","uri":"/%E9%98%9F%E5%88%97%E5%92%8C%E6%A0%88%E8%AF%A6%E8%A7%A33%E9%81%93%E6%8B%AC%E5%8F%B7%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"1 递归反转整个链表 ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:1:0","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 206. 反转链表 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 示例: 输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1] ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:1:1","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"解析 图解: 代码实现: public ListNode reverseList(ListNode head) { if(head==null || head.next==null){ return head; } ListNode last=reverseList(head.next); head.next.next=head; head.next=null; return last; } 2个注意点: 🟡 递归函数要有 base case,如果链表为空或者只有⼀个节点的时候,反转结果就是它自己。 if (head == null || head.next == null) { return head; } 🟢 当链表递归反转之后,新的头结点是 last,而之前的 head 变成了最后⼀个节点,别忘了链表的末尾要指向 null。 head.next = null; ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:1:2","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"2 递归反转前 N 个节点 图解: 代码实现: ListNode successor = null; // 后驱节点 // 反转以 head 为起点的 n 个节点,返回新的头结点 ListNode reverseN(ListNode head, int n) { if (n == 1) { // 记录第 n + 1 个节点 successor = head.next; return head; } // 以 head.next 为起点,需要反转前 n - 1 个节点 ListNode last = reverseN(head.next, n - 1); head.next.next = head; // 让反转之后的 head 节点和后⾯的节点连起来 head.next = successor; return last; } 2 个区别: 🟠 base case 变为 n == 1,反转⼀个元素,就是它本身,同时要记录后驱节点。 🔵 刚才我们直接把 head.next 设置为 null,因为整个链表反转后原来的 head 变成了整个链表的最后⼀个节点。但现在 head 节点在递归反转之后不⼀定是最后⼀个节点了,所以要记录后驱 successor(第 n + 1 个节点),反转之后将 head 连接上。 ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:2:0","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"3 递归反转部分节点 ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:3:0","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 92. 反转链表 II 给你单链表的头指针 head 和两个整数 left 和 right ,其中 left \u003c= right 。 请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。 示例: 输入:head = [1,2,3,4,5], left = 2, right = 4 输出:[1,4,3,2,5] ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:3:1","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"解析 图解: 🟣 如果 m == 1,就相当于反转链表开头的 n 个元素,也就是刚刚实现的 reverseN () 。 🟤 如果 m != 1 ,我们把 head 的索引视为 1,即从第 m 个元素开始反转;如果把 head.next 的索引视为 1 ,那么反转的区间应该从第 m - 1 个元素开始;以此类推…… 代码实现: ListNode successor = null; public ListNode reverseBetween(ListNode head, int left, int right) { // base case if(left==1) return reverseN(head,right-left+1); // 前进到反转的起点触发 base case head.next=reverseBetween(head.next,left-1,right-1); return head; } 💥💥💥 递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)。 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-12","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/:3:2","tags":["labuladong","算法","反转链表","递归"],"title":"【数组链表】递归反转链表","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E9%80%92%E5%BD%92%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 870. 优势洗牌 给定两个大小相等的数组 nums1 和 nums2,nums1 相对于 nums2 的优势可以用满足 nums1[i] \u003e nums2[i] 的索引 i 的数目来描述。 返回 nums1 的任意排列,使其相对于 nums2 的优势最大化。 ","date":"2023-01-08","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E7%94%B0%E5%BF%8C%E8%B5%9B%E9%A9%AC%E8%83%8C%E5%90%8E%E7%9A%84%E7%AE%97%E6%B3%95%E5%86%B3%E7%AD%96/:1:0","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】田忌赛马背后的算法决策","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E7%94%B0%E5%BF%8C%E8%B5%9B%E9%A9%AC%E8%83%8C%E5%90%8E%E7%9A%84%E7%AE%97%E6%B3%95%E5%86%B3%E7%AD%96/"},{"categories":["labuladong的算法秘籍"],"content":"解析 这道题类似于【田忌赛马】,只不过马的数量变多了,精髓在于【打得过就打,打不过就拿自己的垃圾和对方的精锐互换 】。我们先分析【田忌赛马】,考虑以下 3 点: 🟡 如果田忌的 1 号选手 \u003c 齐王的 1 号选手,显然应该用田忌垫底的马送人头,降低对方的战斗力。 🟢 如果田忌的 1 号选手 \u003c 齐王的 1 号选手,则应该直接让两者相比。 🟠 当出现第二种情况时,即 T1 \u003e Q1 时,要不要节约战斗力,用 T2 对抗 Q1? 答案是不需要。假设 T2 \u003e Q1,那么不论换不换 T1,T1 和 T2 都能对抗所有的 Q,这种节约毫无意义。 根据以上思路,我们的策略是: 将齐王和田忌的马按照战斗力排序,然后按照排名一一对比。如果田忌的马能赢,那就比赛,如果赢不了,那就换个垫底的来送人头,保存实力。 结合已学过的双指针技巧,代码实现如下: class Solution { public int[] advantageCount(int[] nums1, int[] nums2) { int n=nums1.length; // 给 nums2 降序排序 PriorityQueue\u003cint[]\u003e maxq = new PriorityQueue\u003c\u003e( (int[] pair1, int[] pair2) -\u003e { return pair2[1] - pair1[1]; } ); for(int i=0; i\u003cn; i++) maxq.offer(new int[] {i, nums2[i]}); // 给 numsq 升序排序 Arrays.sort(nums1); int left=0, right=n-1; int[] res = new int[n]; while(!maxq.isEmpty()){ int[] pair=maxq.poll(); // val 是nums2 中的最大值,i 是对应索引 int i=pair[0], val=pair[1]; if(val\u003cnums1[right]){ // 最大值能比过就直接比 res[i]=nums1[right--]; } else{ // 否则用最小值和最大值比 res[i]=nums1[left++]; } } return res; } } 算法的时间复杂度,也就是二叉堆和排序的复杂度 O(nlogn)。 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-08","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E7%94%B0%E5%BF%8C%E8%B5%9B%E9%A9%AC%E8%83%8C%E5%90%8E%E7%9A%84%E7%AE%97%E6%B3%95%E5%86%B3%E7%AD%96/:2:0","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】田忌赛马背后的算法决策","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E7%94%B0%E5%BF%8C%E8%B5%9B%E9%A9%AC%E8%83%8C%E5%90%8E%E7%9A%84%E7%AE%97%E6%B3%95%E5%86%B3%E7%AD%96/"},{"categories":["labuladong的算法秘籍"],"content":"二分思维的精髓就是:通过已知信息尽可能多地收缩搜索空间,从而增加穷举效率,快速找到目标。 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:0:0","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"1 二分查找框架 int binarySearch(int[] nums, int target) { int left = 0, right = ...; while(...) { int mid = left + (right - left) / 2; if (nums[mid] == target) { ... } else if (nums[mid] \u003c target) { left = ... } else if (nums[mid] \u003e target) { right = ... } } return ...; } 计算 mid 时需要防止溢出,代码中 left + (right - left) / 2 就和 (left + right) / 2 的结果相同,但是有效防止了 left 和 right 太大,直接相加导致溢出的情况。 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:1:0","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"2 寻找一个数 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:2:0","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 704. 二分查找 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:2:1","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"解析 int binarySearch(int[] nums, int target) { int left = 0; int right = nums.length - 1; // 注意 while(left \u003c= right) { int mid = left + (right - left) / 2; if(nums[mid] == target) return mid; else if (nums[mid] \u003c target) left = mid + 1; // 注意 else if (nums[mid] \u003e target) right = mid - 1; // 注意 } return -1; } 🟠 while 循环的条件中是 \u003c=,而不是 \u003c 因为初始化 right 的赋值是 nums.length - 1,而不是 nums.length。前者相当于闭区间 [left, right],后者相当于左闭右开区间 [left, right),因为索引大小为 nums.length 是越界的。 而只有搜索区间为空时才停止寻找,即 left \u003c= right 时都应该继续循环。 🟡 算法缺陷 不能给出左侧 / 右侧边界。例如有序数组 nums = [1,2,2,2,3],target 为 2,左侧索引为 1,右侧索引为 3 。按照该算法只能求出索引 2。 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:2:2","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"3 寻找左侧边界的二分搜索 int left_bound(int[] nums, int target) { int left = 0, right = nums.length - 1; // 搜索区间为 [left, right] while (left \u003c= right) { int mid = left + (right - left) / 2; if (nums[mid] \u003c target) { // 搜索区间变为 [mid+1, right] left = mid + 1; } else if (nums[mid] \u003e target) { // 搜索区间变为 [left, mid-1] right = mid - 1; } else if (nums[mid] == target) { // 收缩右侧边界 right = mid - 1; } } // 判断 target 是否存在于 nums 中 // 此时 target ⽐所有数都⼤,返回 -1 if (left == nums.length) return -1; // 判断⼀下 nums[left] 是不是 target return nums[left] == target ? left : -1; } ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:3:0","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"4 寻找右侧边界的二分查找 int right_bound(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left \u003c= right) { int mid = left + (right - left) / 2; if (nums[mid] \u003c target) { left = mid + 1; } else if (nums[mid] \u003e target) { right = mid - 1; } else if (nums[mid] == target) { // 这⾥改成收缩左侧边界即可 left = mid + 1; } } // 最后改成返回 left - 1 if (left - 1 \u003c 0) return -1; return nums[left - 1] == target ? (left - 1) : -1; } ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:4:0","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"5 综合 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:5:0","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 34. 在排序数组中查找元素的第一个和最后一个位置 给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。 如果数组中不存在目标值 target,返回 [-1, -1]。 你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。 ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:5:1","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"解析 综合使用上面的左侧二分查找、右侧二分查找即可。代码实现如下:(左侧二分查找和右侧二分查找代码省略,上面已有) class Solution { public int[] searchRange(int[] nums, int target) { int index1=left_bound(nums,target); int index2=right_bound(nums,target); return new int[] {index1,index2}; } } 以上就是二分查找算法的细节。在写二分查找代码时,注意以下 4 点: 🟢 分析二分查找代码时,不要出现 else,全部展开成 else if 方便理解。 🔵 注意「搜索区间」和 while 的终止条件,如果存在漏掉的元素,记得在最后检查。 🟣 如需定义左闭右开的「搜索区间」搜索左右边界,只要在 nums[mid] == target 时做修改即可,搜索右侧时需要减一。 🟤 如果将「搜索区间」全都统一成两端都闭,好记,只要稍改 nums[mid] == target 条件处的代码和返回的逻辑即可。 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-07","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/:5:2","tags":["labuladong","算法","二分"],"title":"【数组链表】二分查找算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"本文主要记录最难掌握的一类双指针技巧——滑动窗口算法。算法思路很简单,就是维护一个窗口,不断滑动,时间复杂度为 O(N)。大致逻辑如下: int left = 0, right = 0; while (right \u003c s.size()) { // 增⼤窗⼝ window.add(s[right]); right++; while (window needs shrink) { // 缩⼩窗⼝ window.remove(s[left]); left++; } } 滑动窗口算法的代码框架如下: /* 滑动窗⼝算法框架 */ void slidingWindow(string s) { unordered_map\u003cchar, int\u003e window; int left = 0, right = 0; while (right \u003c s.size()) { // c 是将移⼊窗⼝的字符 char c = s[right]; // 增⼤窗⼝ right++; // 进⾏窗⼝内数据的⼀系列更新 ... /*** debug 输出的位置 ***/ printf(\"window: [%d, %d)\\n\", left, right); /********************/ // 判断左侧窗⼝是否要收缩 while (window needs shrink) { // d 是将移出窗⼝的字符 char d = s[left]; // 缩⼩窗⼝ left++; // 进⾏窗⼝内数据的⼀系列更新 ... } } } unordered_map 就是哈希表(字典),相当于 Java 的 HashMap,它的⼀个方法 count(key) 相当于 Java 的 containsKey(key) 可以判断键 key 是否存在。可以使用方括号访问键对应的值 map[key]。需要注意的是,如果该 key 不存在,C++ 会自动创建这个 key,并把 map[key] 赋值为 0。 套模板前需要思考 3 个问题: ✅ 什么时候移动 right 扩大窗口? ✅ 什么时候移动 left 缩小窗口? ✅ 什么时候更新结果? ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:0:0","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"1 最小覆盖子串 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:1:0","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 76. 最小覆盖子串 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 \"\" 。 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:1:1","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"解析 🟡 我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。 理论上区间可以两端都开或者两端都闭,但左闭右开最放便处理。因为初始化时区间 [0, 0) 没有元素,但 right 向右移动一位,区间 [0, 1) 就包含一个元素 0 了。如果区间的两端都开,那么 right 向右移动一位后区间 (0, 1) 仍没有元素;如果区间两端都闭,那么 [0, 0] 未初始化就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。 🟢 不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求 🔵 停止增加 right,转而不断增加 left 指针缩小窗口[left, right),直到窗口中的字符串不再符合要求。同时,每次增加 left,都要更新一轮结果。 🟣 重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。 如果⼀个字符进入窗口,应该增加 window 计数器;如果⼀个字符将移出窗口的时候,应该减少 window 计数器;当 count 满足 need 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。 class Solution { public: string minWindow(string s, string t) { unordered_map\u003cchar, int\u003e need, window; for(char c:t) need[c]++; int left=0,right=0; // 记录最小覆盖子串的起始索引及长度 int start=0,len=INT_MAX; // 记录窗口内含有规定字符的个数(无重复) int count=0; while(right\u003cs.size()){ char c=s[right]; // 移入窗口的字符 right++; // 扩大窗口 if(need.count(c)){ window[c]++; if(window[c]==need[c]) count++; } // 判断窗口是否要缩小 while(count==need.size()){ // 更新最小覆盖子串 if(right-left \u003c len){ start=left; len = right-left; } char a=s[left]; // 移出窗口的字符 left++; // 缩小窗口 // 更新数据 if(need.count(a)){ if(window[a]==need[a]) count--; window[a]--; } } } return len==INT_MAX ? \"\" : s.substr(start,len); } }; ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:1:2","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"2 字符串的排列 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:2:0","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 567. 字符串的排列 给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。 换句话说,s1 的排列之一是 s2 的 子串 。 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:2:1","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"解析 本题移动 left 缩小窗口 的时机是窗口大小大于 t.size() 时,因为排列嘛,显然长度应该是⼀样的。 当发现 valid == need.size() 时,就说明窗口中就是⼀个合法的排列,所以立即返回 true。 至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同 class Solution { public: bool checkInclusion(string s1, string s2) { unordered_map\u003cchar, int\u003e need, window; for (char c : s1) need[c]++; int left=0,right=0; int count=0; while(right\u003cs2.size()){ char a=s2[right]; right++; // 更新窗口数据 if(need.count(a)){ window[a]++; if(window[a]==need[a]) count++; } // 判断左侧窗口是否收缩 while(right-left \u003e= s1.size()){ // 在这⾥判断是否找到了合法的⼦串 if(count==need.size()) return true; char b=s2[left]; left++; // 更新窗口数据 if(need.count(b)){ if(window[b]==need[b]) count--; window[b]--; } } } return false; } }; ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:2:2","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"3 找所有字母异位词 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:3:0","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 438. 找到字符串中所有字母异位词 给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。 异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:3:1","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"解析 class Solution { public: vector\u003cint\u003e findAnagrams(string s, string p) { unordered_map\u003cchar, int\u003e need, window; for (char c : p) need[c]++; int left=0,right=0; int count=0; vector\u003cint\u003e res; // 记录结果 while(right\u003cs.size()){ char a=s[right]; right++; // 更新窗口数据 if(need.count(a)){ window[a]++; if(window[a]==need[a]) count++; } // 判断左侧窗口是否要收缩 while(right-left\u003e=p.size()){ if(count==need.size()) res.push_back(left); char b=s[left]; left++; // 更新窗口数据 if(need.count(b)){ if(window[b]==need[b]) count--; window[b]--; } } } return res; } }; ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:3:2","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"4 最长无重复子串 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:4:0","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 3. 无重复字符的最长子串 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。 ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:4:1","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"解析 class Solution { public: int lengthOfLongestSubstring(string s) { unordered_map\u003cchar, int\u003e window; int left=0,right=0; int res=0; // 记录结果 while(right\u003cs.size()){ char a=s[right]; right++; // 更新数据 window[a]++; // 判断左侧窗口是否要收缩 while(window[a]\u003e1){ char b=s[left]; left++; window[b]--; // 更新数据 } // 更新答案 res = right-left\u003eres ? right-left : res; } return res; } }; 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-06","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/:4:2","tags":["labuladong","算法","双指针","滑动窗口"],"title":"【数组链表】滑动窗口算法","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%AE%97%E6%B3%95/"},{"categories":["labuladong的算法秘籍"],"content":"在处理数组和链表相关问题时,双指针技巧是经常用到的,双指针技巧主要分为两类:左右指针和快慢指针。所谓左右指针,就是两个指针相向而行或者相背而行;而所谓快慢指针,就是两个指针同向而行。 ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:0:0","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"1 快慢指针技巧 题目 力扣 26. 删除有序数组中的重复项 给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。 将最终结果插入 nums 的前 k 个位置后返回 k 。 不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 解析 我们让慢指针 slow 走在后面,快指针 fast 走在前面探路,找到⼀个不重复的元素就赋值给 slow 并让 slow 前进⼀步。 这样,就保证了 nums[0..slow] 都是无重复的元素,当 fast 指针遍历完整个数组 nums 后,nums[0..slow] 就是整个数组去重之后的结果。 代码实现: class Solution { public int removeDuplicates(int[] nums) { if(nums.length == 0) return 0; int slow=0,fast=0; while(fast\u003cnums.length){ if(nums[slow] != nums[fast]){ slow++; nums[slow]=nums[fast]; } fast++; } return slow+1; } } 进阶1 力扣 83. 删除排序链表中的重复元素 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回已排序的链表 。 代码实现: class Solution { public ListNode deleteDuplicates(ListNode head) { if(head == null) return head; ListNode slow=head,fast=head; while(fast != null){ if(slow.val != fast.val){ slow.next = fast; slow = slow.next; } fast = fast.next; } slow.next = null; return head; } } 进阶2 力扣 27. 移除元素 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。 不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。 解析: 和前面类似,依然需要使用快慢指针技巧。如果 fast 遇到值为 val 的元素,则直接跳过,否则就赋值给 slow 指针,并让 slow 前进⼀步。 代码实现: class Solution { public int removeElement(int[] nums, int val) { int fast = 0, slow = 0; while (fast \u003c nums.length) { if (nums[fast] != val) { nums[slow] = nums[fast]; slow++; } fast++; } return slow; } } 进阶3 力扣 283. 移动零 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 和上一题思路类似,代码实现: class Solution { public void moveZeroes(int[] nums) { int fast = 0, slow = 0; while (fast \u003c nums.length) { if (nums[fast] != 0) { nums[slow] = nums[fast]; slow++; } fast++; } for(int i=slow; i\u003cnums.length; i++) nums[i] = 0; } } ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:1:0","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2 左右指针技巧 ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:2:0","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.1 二分查找 int binarySearch(int[] nums, int target) { // ⼀左⼀右两个指针相向⽽⾏ int left = 0, right = nums.length - 1; while(left \u003c= right) { int mid = (right + left) / 2; if(nums[mid] == target) return mid; else if (nums[mid] \u003c target) left = mid + 1; else if (nums[mid] \u003e target) right = mid - 1; } return -1; } ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:2:1","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.2 两数之和 题目 力扣 167. 两数之和 II - 输入有序数组 给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 \u003c= index1 \u003c index2 \u003c= numbers.length 。以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。 你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。你所设计的解决方案必须只使用常量级的额外空间。 解析 使用双指针,通过调节 left 和 right 就可以调整 sum 的大小。代码实现: class Solution { public int[] twoSum(int[] numbers, int target) { int left=0, right=numbers.length-1; while(left\u003cright){ if(numbers[left]+numbers[right]==target) // 题目要求的索引是从1开始的 return new int[]{left+1,right+1}; else if(numbers[left]+numbers[right]\u003ctarget) left++; // 让和大一点 else right--; // 让和小一点 } return new int[]{-1,-1}; } } ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:2:2","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.3 反转数组 题目 力扣 344. 反转字符串 编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。 不要给另外的数组分配额外的空间,你必须 原地 修改输入数组、使用 O(1) 的额外空间解决这一问题。 解析 通过左右指针反转,思路很简单。 注意:temp 的声明在 while 循环里面内存占用会比下面的多很多。在以后的编程中要注意。 class Solution { public void reverseString(char[] s) { int left=0, right=s.length-1; char temp; while(left\u003cright){ // 交换s[left]和s[right] temp=s[left]; s[left]=s[right]; s[right]=temp; left++; right--; } } } ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:2:3","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2.4 回文串判断 解析 使用左右指针判断回文串,代码如下: boolean isPalindrome(String s) { // ⼀左⼀右两个指针相向⽽⾏ int left = 0, right = s.length() - 1; while (left \u003c right) { if (s.charAt(left) != s.charAt(right)) { return false; } left++; right--; } return true; } 进阶 力扣 5. 最长回文子串 给你一个字符串 s,找到 s 中最长的回文子串。 解析 使用从中心向两端扩散的双指针技巧。如果回文串的长度为奇数,则它有⼀个中心字符;如果回文串的长度为偶数,则可以认为它有两个中心字符。 我们遍历整个数组,并分别求出以 s[i] 和 s[i,j] 为中心的最长回文串,保留两者中更长的,最终可以得到所求结果。 class Solution { public String longestPalindrome(String s) { String res1,res2,res=\"\"; for(int i=0; i\u003cs.length(); i++){ // 以s[i]为中心的最长回文子串 res1 = palindrome(s,i,i); // 以s[i]和s[i+1]为中心的最长回文子串 res2 = palindrome(s,i,i+1); res = res.length()\u003eres1.length() ? res : res1; res = res.length()\u003eres2.length() ? res : res2; } return res; } // 在s中寻找以s[l]和s[r]为中心的最长回文串 public String palindrome(String s, int l, int r){ while(l\u003e=0 \u0026\u0026 r\u003cs.length() \u0026\u0026 s.charAt(l)==s.charAt(r)){ l--; r++; } return s.substring(l+1,r); } } 参考资料: https://labuladong.github.io/algo/ ","date":"2023-01-05","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/:2:4","tags":["labuladong","算法","双指针","数组"],"title":"【数组链表】双指针技巧解决数组题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E6%95%B0%E7%BB%84%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"1 合并两个有序链表 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:1:0","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 21. 合并两个有序链表 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例: 输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4] ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:1:1","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 这题比较简单,直接用 while 循环每次比较 p1 和 p2 的大小,把较小的节点接到结果链表上,如图所示: 虚拟头节点技巧 通过虚拟头节点这个占位符,可以避免处理空指针的情况,降低代码的复杂性。 当你需要创造⼀条新链表的时候,可以使⽤虚拟头结点简化边界情况的处理。 最后通过 p.next = n1; 将剩余的节点全都复制过去,不需要使用 while 循环一条一条复制。 代码实现: class Solution { public ListNode mergeTwoLists(ListNode list1, ListNode list2) { // 虚拟头结点 ListNode p = new ListNode(), res = p; ListNode n1 = list1, n2 = list2; while(n1!=null \u0026\u0026 n2!=null){ // ⽐较 n1 和 n2 两个指针 // 将值较⼩的的节点接到 p 指针 if(n1.val \u003c n2.val){ p.next = n1; n1 = n1.next; } else { p.next = n2; n2 = n2.next; } // p 指针不断前进 p = p.next; } // 最后还剩的直接一次复制过去 if(n1 != null){ p.next = n1; } if(n2!=null){ p.next = n2; } return res.next; } } ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:1:2","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"2 单链表的分解 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:2:0","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 86. 分隔链表 给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你应当 保留 两个分区中每个节点的初始相对位置。 示例: 输入:head = [1,4,3,2,5,2], x = 3 输出:[1,2,2,4,3,5] ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:2:1","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 把原链表分成两个小链表,⼀个链表中的元素大小都小于 x,另⼀个链表中的元素都大于等于 x,最后再把这两条链表接到⼀起,就得到了题目想要的结果。 ❗❗❗注意:最后合并时,要先断开 bp 的 next 指针。因为 bp 是直接由 hp 复制过来的,后面可能还接着小于 x 的节点,因此可能在答案链表中形成循环。 class Solution { public ListNode partition(ListNode head, int x) { // 存放大于 x 的链表的虚拟头节点 ListNode big = new ListNode(), bp = big; // 存放小于 x 的链表的虚拟头节点 ListNode res = new ListNode(), rp = res; ListNode hp = head; // 用于遍历head // 将一个链表分解成2个 while(hp != null){ if(hp.val \u003c x){ rp.next = hp; // 利用虚拟节点 rp = rp.next; } else{ bp.next = hp; // 利用虚拟节点 bp = bp.next; } hp = hp.next; } bp.next = null; // 断开bp的next指针 rp.next = big.next; // 合并2个链表 return res.next; } } ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:2:2","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"3 合并 k 个有序链表 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:3:0","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 23. 合并K个升序链表 给你一个链表数组,每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中,返回合并后的链表。 示例 : 输入:lists = [[1,4,5],[1,3,4],[2,6]] 输出:[1,1,2,3,4,4,5,6] 解释:链表数组如下: [ 1-\u003e4-\u003e5, 1-\u003e3-\u003e4, 2-\u003e6 ] 将它们合并到一个有序链表中得到。 1-\u003e1-\u003e2-\u003e3-\u003e4-\u003e4-\u003e5-\u003e6 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:3:1","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 合并 k 个有序链表的逻辑类似合并两个有序链表,难点在于,如何快速得到 k 个节点中的最小节点,接到结果链表上? 这里我们就要用到 优先级队列(二叉堆) 这种数据结构,把链表节点放入⼀个最小堆,就可以每次获得 k 个节点中的最小节点: ListNode mergeKLists(ListNode[] lists) { if (lists.length == 0) return null; // 虚拟头结点 ListNode dummy = new ListNode(-1); ListNode p = dummy; // 优先级队列,最⼩堆 PriorityQueue\u003cListNode\u003e pq = new PriorityQueue\u003c\u003e( lists.length, (a, b)-\u003e(a.val - b.val)); // 将 k 个链表的头结点加⼊最⼩堆 for (ListNode head : lists) { if (head != null) pq.add(head); } while (!pq.isEmpty()) { // 获取最⼩节点,接到结果链表中 ListNode node = pq.poll(); p.next = node; if (node.next != null) { pq.add(node.next); } // p 指针不断前进 p = p.next; } return dummy.next; } 优先队列 pq 中的元素个数最多是 k,所以⼀次 poll 或者 add 方法的时间复杂度是 O(logk);所有的链表节点都会被加入和弹出 pq,所以算法整体的时间复杂度是 O(Nlogk),其中 k 是链表的条数,N 是这些链表的节点总数。 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:3:2","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"4 单链表的倒数第 k 个节点 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:0","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目—查找链表倒数第N个节点 寻找从后往前数的第 k 个节点。 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:1","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析1 一般做法:遍历 2 次链表 假设链表有 n 个节点,倒数第 k 个节点就是正数第 n - k + 1 个节点。由于算法题⼀般只给⼀个 ListNode 头结点代表⼀条单链表,所以不能直接得出这条链表的长度 n,需要先遍历⼀遍链表算出 n 的值,然后再遍历链表计算第 n - k + 1 个节点。也就是说,这个解法需要遍历两次链表才能得到出倒数第 k 个节点。 改进做法:遍历 1 次链表 首先,我们先让⼀个指针 p1 指向链表的头节点 head,然后走 k 步;再用⼀个指针 p2 指向链表头节点 head ,则 p1 和 p2 之间相差 k 步。当 p1 走到末尾空指针时,p2 刚好在倒数第 k 个的位置。如图所示: 代码实现如下: // 返回链表的倒数第 k 个节点 ListNode findFromEnd(ListNode head, int k) { ListNode p1 = head; // p1 先⾛ k 步 for (int i = 0; i \u003c k; i++) { p1 = p1.next; } ListNode p2 = head; // p1 和 p2 同时⾛ n - k 步 while (p1 != null) { p2 = p2.next; p1 = p1.next; } // p2 现在指向第 n - k + 1 个节点,即倒数第 k 个节点 return p2; } ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:2","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目—删除链表的倒数第N个结点 力扣 19. 删除链表的倒数第 N 个结点 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 示例: 输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:3","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 和上面思路类似,获得倒数第 n + 1 个节点的引用并进行删除。注意使用虚节点技巧,防止出现空指针的情况。代码如下: class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode p1 = new ListNode(); // 虚拟头结点 p1.next = head; // 删除倒数第 n 个,要先找到倒数第 n+1 个节点 ListNode p2 = findFromEnd(p1, n+1); p2.next = p2.next.next; return p1.next; } public ListNode findFromEnd(ListNode head, int n){ ListNode p1 = head; ListNode p2 = head; for(int i=0; i\u003cn; i++){ p1 = p1.next; } while(p1 != null){ p1 = p1.next; p2 = p2.next; } return p2; } } 5 单链表的中点 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:4","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 876. 链表的中间结点 给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。 示例 1: 输入:[1,2,3,4,5] 输出:此列表中的结点 3 (序列化形式:[3,4,5]) 返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。 注意,我们返回了一个 ListNode 类型的对象 ans,这样: ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL. ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:5","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 一般做法:遍历 2 次链表 需要遍历一次链表计算长度 n ,再遍历⼀次得到第 n / 2 个节点,也就是中间节点。 改进做法:遍历 1 次链表 使用【快慢指针】的技巧。我们让两个指针 slow 和 fast 分别指向链表头结点 head。每当慢指针 slow 前进⼀步,快指针 fast 就前进两步,这样,当 fast ⾛到链表末尾时,slow 就指向了链表中点。 代码实现如下: class Solution { ListNode middleNode(ListNode head) { // 快慢指针初始化指向 head ListNode slow = head, fast = head; // 快指针⾛到末尾时停⽌ while (fast != null \u0026\u0026 fast.next != null) { // 慢指针⾛⼀步,快指针⾛两步 slow = slow.next; fast = fast.next.next; } // 慢指针指向中点 return slow; } } 6 判断链表是否包含环 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:6","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 使用【快慢指针】。如果 fast 最终遇到空指针,说明链表中没有环;如果 fast 最终和 slow 相遇,那肯定是 fast 超过了 slow ⼀圈,说明链表中含有环。原理如图所示: boolean hasCycle(ListNode head) { // 快慢指针初始化指向 head ListNode slow = head, fast = head; // 快指针⾛到末尾时停⽌ while (fast != null \u0026\u0026 fast.next != null) { // 慢指针⾛⼀步,快指针⾛两步 slow = slow.next; fast = fast.next.next; // 快慢指针相遇,说明含有环 if (slow == fast) { return true; } } // 不包含环 return false; } ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:7","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"进阶 如果链表中含有环,如何计算这个环的起点? 当快慢指针相遇时,让其中任⼀个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所 在的节点位置就是环开始的位置。 代码如下: ListNode detectCycle(ListNode head) { ListNode fast, slow; fast = slow = head; while (fast != null \u0026\u0026 fast.next != null) { fast = fast.next.next; slow = slow.next; if (fast == slow) break; } // 上⾯的代码类似 hasCycle 函数 if (fast == null || fast.next == null) { // fast 遇到空指针说明没有环 return null; } // 重新指向头结点 slow = head; // 快慢指针同步前进,相交点就是环起点 while (slow != fast) { fast = fast.next; slow = slow.next; } return slow; } 7 两个链表是否相交 ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:8","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 160. 相交链表 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。题目数据 保证 整个链式结构中不存在环。注意,函数返回结果后,链表必须 保持其原始结构 。图示两个链表在节点 c1 开始相交: ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:9","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"解析 一般做法: 用 HashSet 记录⼀个链表的所有节点,然后和另⼀条链表对比,但这就需要额外的空间。 改进做法: 如果用两个指针 p1 和 p2 分别在两条链表上前进,并不能同时走到公共节点,也就无法得到相交节点 c1。 解决这个问题的关键是,通过某些方式,让 p1 和 p2 能够同时到达相交节点 c1。 所以,我们可以让 p1 遍历完链表 A 之后开始遍历链表 B,让 p2 遍历完链表 B 之后开始遍历链表 A,这样相当于【逻辑上】两条链表接在了⼀起。 p1 和 p2 就可以同时进入公共部分。图解如下: 代码实现如下: ListNode getIntersectionNode(ListNode headA, ListNode headB) { // p1 指向 A 链表头结点,p2 指向 B 链表头结点 ListNode p1 = headA, p2 = headB; while (p1 != p2) { // p1 ⾛⼀步,如果⾛到 A 链表末尾,转到 B 链表 if (p1 == null) p1 = headB; else p1 = p1.next; // p2 ⾛⼀步,如果⾛到 B 链表末尾,转到 A 链表 if (p2 == null) p2 = headA; else p2 = p2.next; } return p1; } 参考资料: https://labuladong.github.io/algo/ ","date":"2022-12-31","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/:4:10","tags":["labuladong","算法","双指针","链表"],"title":"【数组链表】双指针技巧解决链表题","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%8F%8C%E6%8C%87%E9%92%88%E6%8A%80%E5%B7%A7%E8%A7%A3%E5%86%B3%E9%93%BE%E8%A1%A8%E9%A2%98/"},{"categories":["labuladong的算法秘籍"],"content":"差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。 ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:0:0","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"1 原理 ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:0","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"题目 给出⼀个数组 nums,要求给区间 nums[2..6] 全部加 1,再给 nums[3..9] 全部减3,再给 nums[0..4] 全部加 2……N步操作后问,最后 nums 数组的值是什么? ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:1","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"解析 常规思路: 用for循环都给 nums[i…j] 加上 val ,时间复杂度为 O(N)。由于对 nums 频繁修改,效率很低。 差分数组: 对 nums 数组构造⼀个 diff 差分数组,diff[i] 就是 nums[i] 和 nums[i-1] 之差。原理如图: 这样构造差分数组 diff,就可以快速进行区间增减的操作,如果你想对区间 nums[i..j] 的元素全部加 3,那么只需要让 diff[i] += 3,然后再让 diff[j+1] -= 3 即可。 只要花费 O(1) 的时间修改 diff 数组,就相当于给 nums 的整个区间做了修改。多次修改 diff,然后通过 diff 数组反推,即可得到 nums 修改后的结果。 代码实现如下: // 差分数组⼯具类 class Difference { // 差分数组 private int[] diff; /* 输⼊⼀个初始数组,区间操作将在这个数组上进⾏ */ public Difference(int[] nums) { assert nums.length \u003e 0; diff = new int[nums.length]; // 根据初始数组构造差分数组 diff[0] = nums[0]; for (int i = 1; i \u003c nums.length; i++) { diff[i] = nums[i] - nums[i - 1]; } } /* 给闭区间 [i, j] 增加 val(可以是负数)*/ public void increment(int i, int j, int val) { diff[i] += val; if (j + 1 \u003c diff.length) { diff[j + 1] -= val; } } /* 返回结果数组 */ public int[] result() { int[] res = new int[diff.length]; // 根据差分数组构造结果数组 res[0] = diff[0]; for (int i = 1; i \u003c diff.length; i++) { res[i] = res[i - 1] + diff[i]; } return res; } } 注意 increment ⽅法中的 if 语句:当 j+1 \u003e= diff.length 时,说明是对 nums[i] 及以后的整个数组都进⾏修改,那么就不需要再给 diff 数组减 val 了。 ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:1:2","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"2 延伸——区间加法 ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:2:0","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 307. 区间加法 假设你有一个长度为 n 的数组,初始情况下所有数字均为 0 ,你将会被给出 k 个更新的操作。 其中,每个操作会被表示为一个三元组:[startIndex, endIndex, inc],你需要将子数组 A[startIndex ... endIndex](包括 startIndex 和 endIndex )增加 inc。 请返回 k 次操作后的数组。 示例: 输入: length = 5, update = [[1,3,2],[2,4,3],[0,2,-2]] 输出: [-2,0,3,5,3] 解释: 初始状态:[0,0,0,0,0] 进行了操作[1,3,2]后的状态:[0,2,2,2,0] 进行了操作[2,4,3]后的状态:[0,2,5,5,3] 进行了操作[0,2,-2]后的状态:[-2,0,3,5,3] ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:2:1","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"解析 使用刚才的 Difference 类: int[] getModifiedArray(int length, int[][] updates) { // nums 初始化为全 0 int[] nums = new int[length]; // 构造差分解法 Difference df = new Difference(nums); for (int[] update : updates) { int i = update[0]; int j = update[1]; int val = update[2]; df.increment(i, j, val); } return df.result(); } ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:2:2","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"3 延伸——航班预订系统 ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:3:0","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 1109. 航班预订统计 这里有 n 个航班,它们分别从 1 到 n 进行编号。 有一份航班预订表 bookings ,表中第 i 条预订记录 bookings[i] = [firsti, lasti, seatsi] 意味着在从 firsti 到 lasti (包含 firsti 和 lasti )的 每个航班 上预订了 seatsi 个座位。 请你返回一个长度为 n 的数组 answer,里面的元素是每个航班预定的座位总数。 示例 : 输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5 输出:[10,55,45,25,25] 解释: 航班编号 1 2 3 4 5 预订记录 1 : 10 10 预订记录 2 : 20 20 预订记录 3 : 25 25 25 25 总座位数: 10 55 45 25 25 因此,answer = [10,55,45,25,25] ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:3:1","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"解析 题目相当于:输入一个长度为 n 的数组 nums,其中所有元素都是 0。再给你输⼊⼀个 bookings,里面是若干三元组 (i, j, k),每个三元组的含义就是要求你给 nums 数组的闭区间 [i-1,j-1] 中所有元素都加上 k。请你返回最后的 nums 数组。 PS:因为题⽬说的 n 是从 1 开始计数的,⽽数组索引从 0 开始,所以对于输⼊的三元组 (i, j, k),数组区间应该对应 [i-1,j-1]。 这是一道标准的差分数组,利用上面的思想很容易实现: class Solution { private int[] diff; public int[] corpFlightBookings(int[][] bookings, int n) { // 构造差分数组 diff = new int[n]; for(int i=0; i\u003cbookings.length; i++){ increment(bookings[i][0],bookings[i][1],bookings[i][2],n); } return result(n); } public void increment(int first, int last, int seat, int n){ diff[first-1] += seat; if(last-1 \u003c n-1){ diff[last] -= seat; } } public int[] result(int n){ int[] res = new int[n]; res[0] = diff[0]; for(int i=1; i\u003cn; i++){ res[i] = diff[i]+res[i-1]; } return res; } } ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:3:2","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"4 延伸——拼车 ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:4:0","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 1094. 拼车 车上最初有 capacity 个空座位。车 只能 向一个方向行驶(也就是说,不允许掉头或改变方向) 给定整数 capacity 和一个数组 trips , trip[i] = [numPassengersi, fromi, toi] 表示第 i 次旅行有 numPassengersi 乘客,接他们和放他们的位置分别是 fromi 和 toi 。这些位置是从汽车的初始位置向东的公里数。 当且仅当你可以在所有给定的行程中接送所有乘客时,返回 true,否则请返回 false。 示例 : 输入:trips = [[2,1,5],[3,3,7]], capacity = 4 输出:false ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:4:1","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"解析 旅客的上车和下车就相当于数组的区间加减;只要结果数组中的元素都小于 capacity,就说明可以不超载运输所有旅客。 题目转化为差分数组,差分数组的长度即车站区间的个数,为1001。result 数组的值即为每段路程车上的人数。图解如下: 代码实现: class Solution { private int[] diff; private int size = 1005; public boolean carPooling(int[][] trips, int capacity) { diff = new int[size]; for(int i=0; i\u003ctrips.length; i++){ increment(trips[i][1],trips[i][2]-1,trips[i][0]); } return result(capacity); } public void increment(int from, int to, int val){ diff[from] += val; if(to \u003c 1000){ diff[to+1] -= val; } } public boolean result(int capacity){ int[] result = new int[size]; result[0] = diff[0]; if(result[0] \u003e capacity) return false; // 客⻋⾃始⾄终都不应该超载 for(int i=1; i\u003c=1000; i++){ result[i] = result[i-1] + diff[i]; if(result[i] \u003e capacity){ return false; } } return true; } } 参考资料: https://labuladong.github.io/algo/ ","date":"2022-12-30","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/:4:2","tags":["labuladong","算法","差分数组"],"title":"【数组链表】差分数组","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/"},{"categories":["labuladong的算法秘籍"],"content":"前缀和主要适⽤的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。 ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:0:0","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"1 一维数组中的前缀和 ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:1:0","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 303. 区域和检索 - 数组不可变 给定一个整数数组 nums,计算索引 left 和 right (包含 left 和 right)之间的 nums 元素的 和 ,其中 left \u003c= right,实现 NumArray 类: NumArray(int[] nums) 使用数组 nums 初始化对象 int sumRange(int i, int j) 返回数组 nums 中索引 left 和 right 之间的元素的 总和 ,包含 left 和 right 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] ) 示例: 输入: [\"NumArray\", \"sumRange\", \"sumRange\", \"sumRange\"] [[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]] 输出: [null, 1, -1, -3] 解释: NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]); numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3) numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1)) numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1)) ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:1:1","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"解析 不使用前缀和的做法: class NumArray { private int[] nums; public NumArray(int[] nums) { this.nums=nums; } public int sumRange(int left, int right) { int sum=0; for(int i=left;i\u003c=right;i++){ sum+=nums[i]; } return sum; } } 可以达到效果,但是效率很差,因为 sumRange ⽅法会被频繁调⽤,⽽它的时间复杂度是 O(N),其中 N 代表 nums 数组的⻓度。使⽤前缀和后, sumRange 函数的时间复杂度降为 O(1),避免使用 for 循环。 前缀和原理: 代码实现: class NumArray { // 前缀和数组 private int[] sum; public NumArray(int[] nums) { sum=new int[nums.length+1]; sum[0]=0; // 计算 nums 的累加和 for(int i=1;i\u003c=nums.length;i++) sum[i]=sum[i-1]+nums[i-1]; } public int sumRange(int left, int right) { return sum[right+1]-sum[left]; } } ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:1:2","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"延伸 这个技巧在⽣活中运⽤也挺⼴泛的,⽐⽅说,你们班上有若⼲同学,每个同学有⼀个期末考试的成绩(满分 100 分),那么请你实现⼀个 API,输⼊任意⼀个分数段,返回有多少同学的成绩在这个分数段内。 那么,你可以先通过计数排序的⽅式计算每个分数具体有多少个同学,然后利⽤前缀和技巧来实现分数段查 询的 API: int[] scores; // 存储着所有同学的分数 // 试卷满分 100 分 int[] count = new int[100 + 1] // 记录每个分数有⼏个同学 for (int score : scores) count[score]++ // 构造前缀和 for (int i = 1; i \u003c count.length; i++) count[i] = count[i] + count[i-1]; // 利⽤ count 这个前缀和数组进⾏分数段查询 ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:1:3","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"2 二维数组中的前缀和 ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:2:0","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"题目 力扣 304. 二维区域和检索 - 矩阵不可变 给定一个二维矩阵 matrix,计算其子矩形范围内元素的总和,该子矩阵的 左上角 为 (row1, col1) ,右下角 为 (row2, col2) 。实现 NumMatrix 类: NumMatrix(int[][] matrix) 给定整数矩阵 matrix 进行初始化 int sumRegion(int row1, int col1, int row2, int col2) 返回 左上角 (row1, col1) 、右下角 (row2, col2) 所描述的子矩阵的元素 总和 。 ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:2:1","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["labuladong的算法秘籍"],"content":"解析 和⼀维数组中的前缀和类似,我们可以维护⼀个⼆维 sum 数组,专⻔记录以原点为顶点的矩阵的元素之和,就可以⽤⼏次加减运算算出任何⼀个⼦矩阵的元素和,典型的 “空间换时间”。思路如图所示: 代码实现: class NumMatrix { // sum[i][j] 记录 matrix 中⼦矩阵 [0, 0, i-1, j-1] 的元素和 private int[][] sum; public NumMatrix(int[][] matrix) { int row=matrix.length; int column=matrix[0].length; sum=new int[row+1][column+1]; // 构造前缀和矩阵 for(int i=1;i\u003c=row;i++){ for(int j=1;j\u003c=column;j++){ // 计算每个矩阵 [0, 0, i, j] 的元素和 sum[i][j]=sum[i][j-1]+sum[i-1][j]-sum[i-1][j-1]+matrix[i-1][j-1]; } } } public int sumRegion(int row1, int col1, int row2, int col2) { return sum[row2+1][col2+1]-sum[row2+1][col1]-sum[row1][col2+1]+sum[row1][col1]; } } 参考资料: https://labuladong.github.io/algo/ ","date":"2022-12-29","objectID":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/:2:2","tags":["labuladong","算法","前缀和"],"title":"【数组链表】前缀和","uri":"/%E6%95%B0%E7%BB%84%E9%93%BE%E8%A1%A8%E5%89%8D%E7%BC%80%E5%92%8C/"},{"categories":["C++"],"content":"1 类\u0026对象 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:0:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.1 成员函数 成员函数可以定义在类定义内部: class Box { public: double length; // 长度 double breadth; // 宽度 double height; // 高度 double getVolume(void) { return length * breadth * height; } }; 也可以在类的外部使用范围解析运算符 :: 定义该函数。 class Box { public: double length; // 长度 double breadth; // 宽度 double height; // 高度 double getVolume(void);// 返回体积 }; double Box::getVolume(void) { return length * breadth * height; } 在 :: 运算符之前必须使用类名。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:1:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.2 类访问修饰符 关键字 public、private、protected 称为访问修饰符。 一个类可以有多个 public、protected 或 private 标记区域。每个标记区域在下一个标记区域开始之前或者在遇到类主体结束右括号之前都是有效的。成员和类的默认访问修饰符是 private。 class Base { public: // 公有成员 protected: // 受保护成员 private: // 私有成员 }; ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:2:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"public(公有成员) 公有成员在程序中类的外部是可访问的。您可以不使用任何成员函数来设置和获取公有变量的值 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:2:1","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"private(私有成员) 私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。默认情况下,类的所有成员都是私有的。 一般会在私有区域定义数据,在公有区域定义相关的函数,以便在类的外部也可以调用这些函数。 #include \u003ciostream\u003eusing namespace std; class Box { public: void setWidth( double wid ); double getWidth( void ); private: double width; }; // 成员函数定义 double Box::getWidth(void) { return width ; } void Box::setWidth( double wid ) { width = wid; } // 程序的主函数 int main( ) { Box box; // 不使用成员函数设置宽度 // box.width = 10.0; // Error: 因为 width 是私有的 box.setWidth(10.0); // 使用成员函数设置宽度 return 0; } ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:2:2","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"protected(受保护成员) **protected(受保护)**成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:2:3","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"继承中的特点 有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。 **public 继承:**基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private **protected 继承:**基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private **private 继承:**基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private 但无论哪种继承方式,上面两点都没有改变: private 成员只能被本类成员(类内)和友元访问,不能被派生类访问; protected 成员可以被派生类访问。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:2:4","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.3 类构造函数\u0026析构函数 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:3:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"构造函数 类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。 构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。 class Line { public: Line(); // 这是构造函数 private: double length; }; // 成员函数定义,包括构造函数 Line::Line(void) { cout \u003c\u003c \"Object is being created\" \u003c\u003c endl; } // 成员函数定义,包括构造函数 Line::Line( double len) { cout \u003c\u003c \"Object is being created, length = \" \u003c\u003c len \u003c\u003c endl; length = len; } ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:3:1","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"使用初始化列表来初始化字段 // 相当于上面带参的构造函数 Line::Line( double len): length(len) { cout \u003c\u003c \"Object is being created, length = \" \u003c\u003c len \u003c\u003c endl; } 假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,同理地,您可以使用上面的语法,只需要在不同的字段使用逗号进行分隔。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:3:2","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"析构函数 类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。 析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。 Line::~Line(void) { cout \u003c\u003c \"Object is being deleted\" \u003c\u003c endl; } ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:3:3","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.4 拷贝构造函数 它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于: 通过使用另一个同类型的对象来初始化新创建的对象。 复制对象把它作为参数传递给函数。 复制对象,并从函数返回这个对象。 如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。拷贝构造函数的最常见形式如下: classname (const classname \u0026obj) { // 构造函数的主体 } 在这里,obj 是一个对象引用,该对象是用于初始化另一个对象的。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:4:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.5 友元函数 类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。 友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。 如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend。 class Box { public: friend void printWidth( Box box ); }; 声明类 ClassTwo 的所有成员函数作为类 ClassOne 的友元,需要在类 ClassOne 的定义中放置如下声明: friend class ClassTwo; ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:5:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.6 this指针 this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。 友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:6:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"1.7 静态成员 当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。 我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化。 class Box { public: static int objectCount; ...... }; // 初始化类 Box 的静态成员 int Box::objectCount = 0; int main(void) { ...... // 输出对象的总数 cout \u003c\u003c \"Total objects: \" \u003c\u003c Box::objectCount \u003c\u003c endl; ...... } ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:7:0","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"静态成员函数 静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。 静态成员函数与普通成员函数的区别: 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。 普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针。 ","date":"2022-12-29","objectID":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/:7:1","tags":["C++","类\u0026对象"],"title":"C++面向对象(一)","uri":"/c-%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%80/"},{"categories":["C++"],"content":"11 字符串 C风格字符串 字符串实际上是使用 null 字符 \\0 终止的一维字符数组。 char site[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\\0'}; char site[] = \"RUNOOB\"; 字符串相关函数: strcpy(s1, s2); 复制字符串 s2 到字符串 s1。 strcat(s1, s2); 连接字符串 s2 到字符串 s1 的末尾。连接字符串也可以用 + 号。 strlen(s1); 返回字符串 s1 的长度。 strcmp(s1, s2); 如果 s1 和 s2 是相同的,则返回 0;如果 s1\u003cs2 则返回值小于 0;如果 s1\u003es2 则返回值大于 0。 strchr(s1, ch); 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。 strstr(s1, s2); 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。 String类 string str1 = \"runoob\"; string str2 = \"google\"; string str3; int len ; // 连接 str1 和 str2 str3 = str1 + str2; // 连接后,str3 的总长度为12! len = str3.size(); ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/:1:0","tags":["C++"],"title":"C++基础语法(三)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["C++"],"content":"12 指针 指针声明 int *ip; /* 一个整型的指针 */ double *dp; /* 一个 double 型的指针 */ float *fp; /* 一个浮点型的指针 */ char *ch; /* 一个字符型的指针 */ 所有指针的值的实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,都是一样的,都是一个代表内存地址的长的十六进制数。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。 Null 指针 在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。 int *ptr = NULL; // ptr的值是0 指针的算术运算 假设 ptr 是一个指向地址 1000 的整型指针,是一个 32 位的整数。 🟡 递增一个指针 ptr++; 在执行完上述的运算之后,ptr 将指向位置 1004,因为 ptr 每增加一次,它都将指向下一个整数位置,即当前位置往后移 4 个字节。这个运算会在不影响内存位置中实际值的情况下,移动指针到下一个内存位置。如果 ptr 指向一个地址为 1000 的字符,上面的运算会导致指针指向位置 1001,因为下一个字符位置是在 1001。 变量指针可以递增,而数组不能递增,因为数组是一个常量指针。 int var[MAX] = {10, 100, 200}; for (int i = 0; i \u003c MAX; i++) { *var = i; // 这是正确的语法 var++; // 这是不正确的,数组指针为常量 } 🔵 指针的比较 指针可以用关系运算符进行比较,如 ==、\u003c 和 \u003e。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。 指向指针的指针 当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。 int var = 300; int *ptr; int **pptr; ptr = \u0026var; // 获取 var 的地址 pptr = \u0026ptr; // 使用运算符 \u0026 获取 ptr 的地址 cout \u003c\u003c \"var 值为 :\" \u003c\u003c var \u003c\u003c endl; // 3000 cout \u003c\u003c \"*ptr 值为:\" \u003c\u003c *ptr \u003c\u003c endl; // 3000 cout \u003c\u003c \"**pptr 值为:\" \u003c\u003c **pptr \u003c\u003c endl; // 3000 传递指针给函数 从函数返回指针 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/:2:0","tags":["C++"],"title":"C++基础语法(三)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["C++"],"content":"13 引用 引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。 C++ 引用 vs 指针 不存在空引用。引用必须连接到一块合法的内存。 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。 引用必须在创建时被初始化。指针可以在任何时间被初始化。 创建引用 \u0026 读作引用。 int i; // 声明简单的变量 // r 是一个初始化为 i 的整型引用 int\u0026 r = i; // 声明引用变量 i = 5; cout \u003c\u003c \"Value of i : \" \u003c\u003c i \u003c\u003c endl; // 5 cout \u003c\u003c \"Value of i reference : \" \u003c\u003c r \u003c\u003c endl; // 5 把引用作为参数 把引用作为返回值 double vals[] = {10.1, 12.6, 33.1, 24.1, 50.0}; double\u0026 setValues(int i) { double\u0026 ref = vals[i]; return ref; // 返回第 i 个元素的引用,ref 是一个引用变量,ref 引用 vals[i] } int main () { setValues(1) = 20.23; // 改变第 2 个元素 cout \u003c\u003c vals[1] \u003c\u003c endl; // 由12.6变为20.23 return 0; } 当返回一个引用时,要注意被引用的对象不能超出作用域。所以返回一个对局部变量的引用是不合法的,但是,可以返回一个对静态变量的引用。 int\u0026 func() { int q; //! return q; // 在编译时发生错误 static int x; return x; // 安全,x 在函数作用域外依然是有效的 } ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/:3:0","tags":["C++"],"title":"C++基础语法(三)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["C++"],"content":"14 日期 \u0026 时间 C++ 标准库没有提供所谓的日期类型。C++ 继承了 C 语言用于日期和时间操作的结构和函数。为了使用日期和时间相关的函数和结构,需要在 C++ 程序中引用 \u003cctime\u003e 头文件。 相关数据类型 有四个与时间相关的类型:clock_t、time_t、size_t 和 tm。类型 clock_t、size_t 和 time_t 能够把系统时间和日期表示为某种整数。 结构类型 tm 把日期和时间以 C 结构的形式保存,tm 结构的定义如下: struct tm { int tm_sec; // 秒,正常范围从 0 到 59,但允许至 61 int tm_min; // 分,范围从 0 到 59 int tm_hour; // 小时,范围从 0 到 23 int tm_mday; // 一月中的第几天,范围从 1 到 31 int tm_mon; // 月,范围从 0 到 11 int tm_year; // 自 1900 年起的年数 int tm_wday; // 一周中的第几天,范围从 0 到 6,从星期日算起 int tm_yday; // 一年中的第几天,范围从 0 到 365,从 1 月 1 日算起 int tm_isdst; // 夏令时 }; 关于日期和时间的重要函数 time_t time(time_t *time); 该函数返回系统的当前日历时间,自 1970 年 1 月 1 日以来经过的秒数。如果系统没有时间,则返回 -1。 char *ctime(const time_t *time); 该返回一个表示当地时间的字符串指针,字符串形式 day month year hours:minutes:seconds year\\n\\0。 struct tm *localtime(const time_t *time); 该函数返回一个指向表示本地时间的 tm 结构的指针。 clock_t clock(void); 该函数返回程序执行起(一般为程序的开头),处理器时钟所使用的时间。如果时间不可用,则返回 -1。 char * asctime ( const struct tm * time ); 该函数返回一个指向字符串的指针,字符串包含了 time 所指向结构中存储的信息,返回形式为:day month date hours:minutes:seconds year\\n\\0。 struct tm *gmtime(const time_t *time); 该函数返回一个指向 time 的指针,time 为 tm 结构,用协调世界时(UTC)也被称为格林尼治标准时间(GMT)表示。 time_t mktime(struct tm *time); 该函数返回日历时间,相当于 time 所指向结构中存储的时间。 double difftime ( time_t time2, time_t time1 ); 该函数返回 time1 和 time2 之间相差的秒数。 size_t strftime(); 该函数可用于格式化日期和时间为指定的格式。 当前日期和时间 time_t now = time(0); // 基于当前系统的当前日期/时间 char* dt = ctime(\u0026now); // 把 now 转换为字符串形式 cout \u003c\u003c \"本地日期和时间:\" \u003c\u003c dt \u003c\u003c endl; // Sat Jan 8 20:07:41 2011 // 把 now 转换为 tm 结构 tm *gmtm = gmtime(\u0026now); dt = asctime(gmtm); cout \u003c\u003c \"UTC 日期和时间:\"\u003c\u003c dt \u003c\u003c endl; // Sat Jan 9 20:07:41 2011 使用结构 tm 格式化时间 // 基于当前系统的当前日期/时间 time_t now = time(0); tm *ltm = localtime(\u0026now); // 输出 tm 结构的各个组成部分 cout \u003c\u003c \"年: \"\u003c\u003c 1900 + ltm-\u003etm_year \u003c\u003c endl; cout \u003c\u003c \"月: \"\u003c\u003c 1 + ltm-\u003etm_mon\u003c\u003c endl; cout \u003c\u003c \"日: \"\u003c\u003c ltm-\u003etm_mday \u003c\u003c endl; ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/:4:0","tags":["C++"],"title":"C++基础语法(三)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["C++"],"content":"15 基本的输入输出 标准输出流(cout) cout 是与流插入运算符 « 结合使用的,« 运算符被重载来输出内置类型(整型、浮点型、double 型、字符串和指针)的数据项。 标准输入流(cin) cin 是与流提取运算符 » 结合使用的。 cin \u003e\u003e name \u003e\u003e age; cout \u003c\u003c \"您的名称是: \" \u003c\u003c name \u003c\u003c endl; cout \u003c\u003c \"您的年龄是: \" \u003c\u003c age \u003c\u003c endl; 标准错误流(cerr) cerr 对象是非缓冲的,且每个流插入到 cerr 都会立即输出。 char str[] = \"Unable to read....\"; cerr \u003c\u003c \"Error message : \" \u003c\u003c str \u003c\u003c endl; // 输出结果为 Error message : Unable to read.... 标准日志流(clog) char str[] = \"Unable to read....\"; clog \u003c\u003c \"Error message : \" \u003c\u003c str \u003c\u003c endl; // 输出结果为 Error message : Unable to read.... ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/:5:0","tags":["C++"],"title":"C++基础语法(三)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["C++"],"content":"16 数据结构 定义结构 struct Books { char title[50]; char author[50]; char subject[100]; int book_id; } book; 访问结构成员 使用成员访问运算符(.)。成员访问运算符是结构变量名称和我们要访问的结构成员之间的一个句号。 Books Book1; // 定义结构体类型 Books 的变量 Book1 // Book1 详述 strcpy( Book1.title, \"C++ 教程\"); strcpy( Book1.author, \"Runoob\"); strcpy( Book1.subject, \"编程语言\"); Book1.book_id = 12345; 结构作为函数参数 void printBook( struct Books book ) { cout \u003c\u003c \"书标题 : \" \u003c\u003c book.title \u003c\u003cendl; cout \u003c\u003c \"书作者 : \" \u003c\u003c book.author \u003c\u003cendl; cout \u003c\u003c \"书类目 : \" \u003c\u003c book.subject \u003c\u003cendl; cout \u003c\u003c \"书 ID : \" \u003c\u003c book.book_id \u003c\u003cendl; } 指向结构的指针 为了使用指向该结构的指针访问结构的成员,您必须使用 -\u003e 运算符. int main( ) { ...... printBook( \u0026Book1 ); // 通过传 Book1 的地址来输出 Book1 信息 ...... } // 该函数以结构指针作为参数 void printBook( struct Books *book ) { cout \u003c\u003c \"书标题 : \" \u003c\u003c book-\u003etitle \u003c\u003cendl; cout \u003c\u003c \"书作者 : \" \u003c\u003c book-\u003eauthor \u003c\u003cendl; cout \u003c\u003c \"书类目 : \" \u003c\u003c book-\u003esubject \u003c\u003cendl; cout \u003c\u003c \"书 ID : \" \u003c\u003c book-\u003ebook_id \u003c\u003cendl; } typedef 关键字 下面是一种更简单的定义结构的方式,您可以为创建的类型取一个\"别名\"。例如: typedef struct Books { char title[50]; char author[50]; char subject[100]; int book_id; }BO; BO Book1, Book2; 您可以使用 typedef 关键字来定义非结构类型,如下所示: typedef long int *pint32; pint32 x; // x是指向长整型 long int 的指针。 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/:6:0","tags":["C++"],"title":"C++基础语法(三)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["C++"],"content":"6 运算符 sizeof运算符 sizeof 运算符可用于获取类、结构、共用体和其他用户自定义数据类型的大小。 逗号运算符 整个逗号表达式的值为系列中最后一个表达式的值。从本质上讲,逗号的作用是将一系列运算按顺序执行。 逗号之前的自增表达式也会在逗号结束后执行! // 运行完结果:count=19,incr=10,var=20 var = (count=19, incr=10, count+1); // 结果:j=11,i=1010 j = 10; i = (j++, j+100, 999+j); 成员运算符 .(点)运算符和 -\u003e(箭头)运算符用于引用类、结构和共用体的成员。访问结构的成员时使用点运算符,而通过指针访问结构的成员时,则使用箭头运算符。例如,假设有下面的结构: struct Employee { char first_name[16]; int age; } emp; 点运算符: strcpy(emp.first_name, \"zara\"); 箭头运算符: // p_emp 是一个指针,指向类型为 Employee 的对象 strcpy(p_emp-\u003efirst_name, \"zara\"); 强制转换运算符 double a = 21.09399; int c = (int) a; // 结果为21 指针运算符 取地址运算符 \u0026:返回操作数的内存地址。 间接寻址运算符 *:返回操作数所指定地址的变量的值。 int var = 3000; int *ptr; int val; ptr = \u0026var; // 获取 var 的地址 val = *ptr; // 获取 ptr 的值 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:0","tags":["C++"],"title":"C++基础语法(二)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["C++"],"content":"7 循环 基于范围的for循环: int my_array[5] = {1, 2, 3, 4, 5}; // 每个数组元素乘于 2 for (int \u0026x : my_array) { x *= 2; cout \u003c\u003c x \u003c\u003c endl; } ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/:2:0","tags":["C++"],"title":"C++基础语法(二)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["C++"],"content":"8 switch语句 switch 语句中的 expression 必须是一个整型或枚举类型,或者是一个 class 类型,其中 class 有一个单一的转换函数将其转换为整型或枚举类型。 不是每一个 case 都需要包含 break。如果 case 语句不包含 break,控制流将会 继续 后续的 case,直到遇到 break 为止。 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/:3:0","tags":["C++"],"title":"C++基础语法(二)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["C++"],"content":"9 函数 函数声明 int max(int num1, int num2); // 在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明: int max(int, int); 当您在一个源文件中定义函数且在另一个文件中调用函数时,函数声明是必需的。在这种情况下,您应该在调用函数的文件顶部声明函数。 函数参数 形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。当调用函数时,有三种向函数传递参数的方式: 传值调用 该方法把参数的实际值赋值给函数的形式参数。在这种情况下,修改函数内的形式参数对实际参数没有影响。 指针调用 把参数的地址复制给形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。 // 调用:swap(\u0026a,\u0026b) void swap(int *x, int *y) { int temp; temp = *x; /* 保存地址 x 的值 */ *x = *y; /* 把 y 赋值给 x */ *y = temp; /* 把 x 赋值给 y */ } 引用调用 把引用的地址复制给形式参数。在函数内,该引用用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。 // 调用:swap(a,b) void swap(int \u0026x, int \u0026y) { int temp; temp = x; /* 保存地址 x 的值 */ x = y; /* 把 y 赋值给 x */ y = temp; /* 把 x 赋值给 y */ } 参数的默认值 定义一个函数,可以为参数列表中后边的每一个参数指定默认值。当调用函数时,如果实际参数的值留空,则使用这个默认值。 int sum(int a, int b=20) { int result; result = a + b; return (result); } Lambda 函数与表达式 对匿名函数的支持,称为 Lambda 函数(也叫 Lambda 表达式)。 Lambda 表达式把函数看作对象。Lambda 表达式可以像对象一样使用,比如可以将它们赋给变量和作为参数传递,还可以像函数一样对其求值。Lambda 表达式具体形式如下: // 一般形式 [capture](parameters)-\u003ereturn-type{body} // 一般情况 [](int x, int y) -\u003e int { int z = x + y; return z + x; } // 无返回类型 [](int x, int y){ return x \u003c y ; } // 无参数 []{ ++global_x; } 在Lambda表达式内可以访问当前作用域的变量,这是Lambda表达式的闭包(Closure)行为。 与JavaScript闭包不同,C++变量传递有传值和传引用的区别。可以通过前面的[]来指定: [] // 沒有定义任何变量。使用未定义变量会引发错误。 [x, \u0026y] // x以传值方式传入(默认),y以引用方式传入。 [\u0026] // 任何被使用到的外部变量都隐式地以引用方式加以引用。 [=] // 任何被使用到的外部变量都隐式地以传值方式加以引用。 [\u0026, x] // x显式地以传值方式加以引用。其余变量以引用方式加以引用。 [=, \u0026z] // z显式地以引用方式加以引用。其余变量以传值方式加以引用。 另外有一点需要注意。对于[=]或[\u0026]的形式,lambda 表达式可以直接使用 this 指针。但是,对于[]的形式,如果要使用 this 指针,必须显式传入: [this]() { this-\u003esomeFunc(); }(); ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/:4:0","tags":["C++"],"title":"C++基础语法(二)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["C++"],"content":"10 数字 数学运算 引用数学头文件 \u003ccmath\u003e可以使用C++内置的数学函数。 double log(double); 该函数返回参数的自然对数。 double pow(double, double); 假设第一个参数为 x,第二个参数为 y,则该函数返回 x 的 y 次方。 double sqrt(double); 该函数返回参数的平方根。 int abs(int); 该函数返回整数的绝对值。 double fabs(double); 该函数返回任意一个浮点数的绝对值。 double floor(double); 该函数返回一个小于或等于传入参数的最大整数。 随机数 关于随机数生成器,有两个相关的函数。一个是 rand(),该函数只返回一个伪随机数。生成随机数之前必须先调用 srand() 函数。 下面是一个关于生成随机数的简单实例。实例中使用了 time() 函数来获取系统时间的秒数,通过调用 rand() 函数来生成随机数: int main () { // 设置种子 srand( (unsigned)time( NULL ) ); // 生成实际的随机数 int j= rand(); return 0; } ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/:5:0","tags":["C++"],"title":"C++基础语法(二)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["C++"],"content":"11 数组 多维数组 int a[3][4] = { {0, 1, 2, 3} , /* 初始化索引号为 0 的行 */ {4, 5, 6, 7} , /* 初始化索引号为 1 的行 */ {8, 9, 10, 11} /* 初始化索引号为 2 的行 */ }; // 内部嵌套的括号是可选的,下面的初始化与上面是等同的: int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11}; 指向数组的指针 double *p; double runoobAarray[10]; // 把 p 赋值为 runoobAarray 的第一个元素的地址 p = runoobAarray; // *(runoobAarray + 4) 是一种访问 runoobAarray[4] 数据的合法方式 // *(runoobAarray + 4) == *(p+4) 传递数组给函数 C++ 传数组给一个函数,数组类型自动转换为指针类型,因而传的实际是地址。 如果您想要在函数中传递一个一维数组作为参数,您必须以下面三种方式来声明函数形式参数,这三种声明方式的结果是一样的,因为每种方式都会告诉编译器将要接收一个整型指针。同样地,您也可以传递一个多维数组作为形式参数。 形式参数是一个指针: void myFunction(int *param){} 形式参数是一个已定义大小的数组: void myFunction(int param[10]){} 形式参数是一个未定义大小的数组: void myFunction(int param[]){} 就函数而言,数组的长度是无关紧要的,因为 C++ 不会对形式参数执行边界检查。 从函数返回数组 C++ 不允许返回一个完整的数组作为函数的参数。但是,您可以通过指定不带索引的数组名来返回一个指向数组的指针。 // 要生成和返回随机数的函数 int * getRandom( ) { static int r[10]; // 设置种子 srand( (unsigned)time( NULL ) ); for (int i = 0; i \u003c 10; ++i) { r[i] = rand(); } return r; } ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/:6:0","tags":["C++"],"title":"C++基础语法(二)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["C++"],"content":"1 C++简介 C++ 完全支持面向对象的程序设计,包括面向对象开发的四大特性:封装、抽象、继承、多态 标准的 C++ 由三个重要部分组成: 核心语言,提供了所有构件块,包括变量、数据类型和常量,等等。 C++ 标准库,提供了大量的函数,用于操作文件、字符串等。 标准模板库(STL),提供了大量的方法,用于操作数据结构等。 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/:1:0","tags":["C++"],"title":"C++基础语法(一)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["C++"],"content":"2 数据类型 基本内置类型 bool,char,int,float,double,void 类型修饰符 signed,unsigned,short,long C++ 允许使用速记符号来声明无符号短整数或无符号长整数。您可以不写 int,只写单词 unsigned、short 或 long。 typedef 声明 可以使用 typedef 为一个已有的类型取一个新的名字。 typedef int feet; 枚举类型 如果一个变量只有几种可能的值,可以定义为枚举(enumeration)类型。所谓\"枚举\"是指将变量的值一一列举出来,变量的值只能在列举出来的值的范围内。 enum 枚举名{ 标识符[=整型常数], ... } 枚举变量; 默认情况下,第一个名称的值为 0,第二个名称的值为 1,第三个名称的值为 2,以此类推。但是,您也可以给名称赋予一个特殊的值,只需要添加一个初始值即可。 enum color { red, green, blue } c; c = blue; // red默认为0,blue默认为6 enum color { red, green=5, blue }; 变量声明 当使用多个文件且只在其中一个文件中定义变量时,可以使用 extern 关键字在任何地方声明一个变量。虽然您可以在 C++ 程序中多次声明一个变量,但变量只能在某个文件、函数或代码块中被定义一次。 #include \u003ciostream\u003eusing namespace std; // 变量声明 extern float f; ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/:2:0","tags":["C++"],"title":"C++基础语法(一)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["C++"],"content":"3 变量作用域 作用域是程序的一个区域,一般来说有三个地方可以定义变量: 在函数或一个代码块内部声明的变量,称为局部变量。 在函数参数的定义中声明的变量,称为形式参数。 在所有函数外部声明的变量,称为全局变量。 局部变量和全局变量的名称可以相同,但是在函数内,局部变量的值会覆盖全局变量的值。 当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动初始化。 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/:3:0","tags":["C++"],"title":"C++基础语法(一)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["C++"],"content":"4 常量 整数常量 前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。 后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long),可以大写也可以小写,U和L的顺序任意。 浮点常量 当使用小数形式表示时,必须包含整数部分、小数部分,或同时包含两者。当使用指数形式表示时, 必须包含小数点、指数,或同时包含两者。带符号的指数是用 e 或 E 引入的。 314159E-5L // 合法的 510E // 非法的:不完整的指数 210f // 非法的:没有小数或指数 .e55 // 非法的:缺少整数或分数 字符串常量 可以使用 “\\” 做分隔符,把一个很长的字符串常量进行分行。 定义常量 使用 #define 预处理器定义常量: #include \u003ciostream\u003eusing namespace std; #define NEWLINE '\\n' int main() { cout \u003c\u003c NEWLINE; return 0; } 使用 const 前缀声明指定类型的常量: int main() { const char NEWLINE = '\\n'; cout \u003c\u003c NEWLINE; return 0; } 注意:尽量把常量定义为大写字母形式。 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/:4:0","tags":["C++"],"title":"C++基础语法(一)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["C++"],"content":"5 存储类 static 存储类 使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。 static 用在类数据成员上时,会导致仅有一个该成员的副本被类的所有对象共享。 extern 存储类 当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。 mutable 存储类 thread_local 存储类 ","date":"2022-12-27","objectID":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/:5:0","tags":["C++"],"title":"C++基础语法(一)","uri":"/c-%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["labuladong的算法秘籍"],"content":"1 数据结构的存储方式 ","date":"2022-12-23","objectID":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/:1:0","tags":["labuladong","算法"],"title":"算法和刷题的框架思维","uri":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/"},{"categories":["labuladong的算法秘籍"],"content":"1.1 数据结构\u0026存储方式 数据的存储方式只有两种:数组(顺序存储)和链表(链式存储)。 【队列】【栈】 【图】:链表实现就是邻接表,二维数组实现就是邻接矩阵 【散列表】 【树】:数组实现就是堆(完全二叉树),链表实现就是普通二叉树 ","date":"2022-12-23","objectID":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/:1:1","tags":["labuladong","算法"],"title":"算法和刷题的框架思维","uri":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/"},{"categories":["labuladong的算法秘籍"],"content":"1.2 数组\u0026链表优缺点 数组: 紧凑连续存储,可以随机访问,通过索引快速找到对应元素,节约存储空间。 内存空间必须⼀次性分配够,扩容时需要重新分配空间,再把数据全部复制过去,时间复杂度 O(N) 插⼊和删除时间复杂度 O(N) 链表: 元素不连续,不存在数组的扩容问题; 如果知道某⼀元素的前驱和后驱,插入删除时间复杂度 O(1) 存储空间不连续,不能随机访问 每个元素必须存储指向前后元素位置的指针,会消耗相对更多的储存空间。 ","date":"2022-12-23","objectID":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/:1:2","tags":["labuladong","算法"],"title":"算法和刷题的框架思维","uri":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/"},{"categories":["labuladong的算法秘籍"],"content":"2 数据结构的基本操作 数据结构的基本操作:遍历+访问(增删查改),分为线性/非线性。 线性即for/while迭代,非线性即递归。 🟡 【数组遍历框架】 迭代 void traverse(int[] arr) { for (int i = 0; i \u003c arr.length; i++) { // 迭代访问 arr[i] } } 🟢 【链表遍历框架】 迭代/递归 /* 基本的单链表节点 */ class ListNode { int val; ListNode next; } void traverse(ListNode head) { for (ListNode p = head; p != null; p = p.next) { // 迭代访问 p.val } } void traverse(ListNode head) { // 递归访问 head.val traverse(head.next); } 🔵 【二叉树遍历框架】 递归 /* 基本的⼆叉树节点 */ class TreeNode { int val; TreeNode left, right; } void traverse(TreeNode root) { traverse(root.left); traverse(root.right); } 🟣 【N叉树遍历框架】 递归 /* 基本的 N 叉树节点 */ class TreeNode { int val; TreeNode[] children; } void traverse(TreeNode root) { for (TreeNode child : root.children) traverse(child); } N 叉树的遍历又可以扩展为图的遍历,因为图就是好几个 N 叉棵树的结合体。图是可能出现环的,用个布尔数组 visited 做标记就行。 ","date":"2022-12-23","objectID":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/:2:0","tags":["labuladong","算法"],"title":"算法和刷题的框架思维","uri":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/"},{"categories":["labuladong的算法秘籍"],"content":"3 算法刷题指南 ✅ 先学习像数组、链表这种基本数据结构的常用算法,比如单链表翻转,前缀和数组,二分搜索等。 因为这些算法属于会者不难难者不会的类型,难度不大,学习它们不会花费太多时间。而且这些小而美的算法经常让你大呼精妙,能够有效培养你对算法的兴趣。 ✅ 学会基础算法之后,不要急着上来就刷回溯算法、动态规划这类笔试常考题,而应该先刷⼆叉树。因为⼆叉树是最容易培养框架思维的,而且⼤部分算法技巧,本质上都是树的遍历问题。 ✅ 刷题试着从框架上看问题,而不要纠结于细节问题。 纠结细节问题,就比如纠结 i 到底应该加到 n 还是加到 n - 1,这个数组的⼤⼩到底应该开 n 还是 n + 1? 参考资料: https://labuladong.github.io/algo/ ","date":"2022-12-23","objectID":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/:3:0","tags":["labuladong","算法"],"title":"算法和刷题的框架思维","uri":"/%E7%AE%97%E6%B3%95%E5%92%8C%E5%88%B7%E9%A2%98%E7%9A%84%E6%A1%86%E6%9E%B6%E6%80%9D%E7%BB%B4/"},{"categories":["Mysql"],"content":"7 并发控制与事务的隔离级别 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:0:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"7.1 并发控制与事务的隔离级别 并发操作可能产生的数据不一致性 数据库是共享资源,允许多个用户同时访问同一数据库,特别是在互联网应用成为主流的当下,高可用性、高并发是所有应用追求的目标。但并发操作不加控制,便会产生数据的不一致性。 并发操作可能带来的数据不一致性包括: 丢失修改(lost update) 读脏数据(dirty read) 不可重复读(non-repeatable read) 幻读(phantom read) 为解决上述不一致性问题,DBMS设计了专门的并发控制子系统,采用封锁机制进行并发控制,以保证事务的隔离性和一致性(事务是并发控制的基本单位)。 但事务的隔离程度越高,固然一致性–或者説数据的正确性越有保障,但并发度就会越低。很多时候,需要在一致性和并发度间进行取舍,从而就生产了事务的隔离级别的概念。 隔离级别越高,一致性程度越高,并发度越低。反之,隔离级别越低,并发度越高,但代价是会出现某些数据不一致现象。 低隔离级别可以支持更高的并发处理,同时占用的系统资源更少,但可能产生数据不一致的情形也更多一些。 查询事务的隔离级别 可用以下语句查询MySQL的事务隔离级别: select @@GLOBAL.transaction_isolation, @@transaction_isolation; 其中,@@GLOBAL.transaction_isolation全局变量,@@transaction_isolation为本会话期内的变量。通常通过重设该变量的值以改变隔离级别。 上述两个变量的缺省值均为:REPEATABLE-READ,即可重复读。 设置事务的隔离级别 以下语句设置事务的隔离级别为可读未提交(read uncommitted): set session transaction isolation level read uncommitted; 如需设置为其它级别,只需替换最后的隔离级别即可。 不同的事务隔离级别意味着不同的封锁协议,程序员只需设置事务的隔离级别即可,其它的交给DBMS并发子系统处理。 不过,MySQL也有lock tables和unlock tables语句,可以直接锁表,另外,MySQL还支持在select语句中使用for share或for update短语主动申请S锁或X锁(只到事务结束才释放)。这样,即使在隔离级别为read uncommitted的情形下,仍有机会保证可重复读,相关内容请参阅MySQL官方文档。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:1:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"7.2 读脏 读脏 读脏(dirty read),或者又叫脏读,是指一个事务(t1)读取到另一个事务(t2)修改后的数据,后来事务t2又撤销了本次修改(即事务t2以roll back结束),数据恢复原值。这样,事务t1读到的数据就与数据库里的实际数据不一致,这样的数据被称为“脏”数据,意即不正确的数据。 读脏产生的原因 显然,产生读脏的原因,是事务t1读取数据时,修改该数据的事务t2还没有结束(commit或roll back,统称uncommitted),且t1读取的时间点又恰在t2修改该数据之后。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:2:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"7.3 不可重复读 不可重复读 不可重复读(unrepeatable read),是指一个事务(t1)读取到某数据后,另一个事务(t2)修改了该,事务t1并未修改该数据,但当t1再次读取该数据时,发现两次读取的结果不一样。 产生不可重复读的原因 显然,不可重复读产生的原因,是事务t1的两次读取之间,有另一个事务修改了t1读取的数据。 根据第一关介绍的基本知识,有两种隔离级别都有可能发生不可重复读。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:3:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"7.4 幻读 幻读(phantom read) 幻读定义其实是有些争议的,在某些文献中,幻读被归为不可重复读(unrepeatable read)中的一类,而另一些则把它与不可重复读区分开来:幻读是指一个事务(t1)读取到某数据后,另一个事务(t2)作了insert或delete操作,事务t1再次读取该数据时,魔幻般地发现数据变多了或者变少了(记录数量不一致);而不可重复读限指事务t2作了update操作,致使t1的两次读操作读到的结果(数据的值)不一致。 产生幻读的原因 显然,幻读产生的原因,是事务t1的两次读取之间,有另一个事务insert或delete了t1读取的数据集。 除了最高级别serializable(可串行化)以外的任何隔离级别,都有可能发生幻读。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:4:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"7.5 MySQL对共享锁与锁的支持 通过设置不同的隔离级别,以实现不同的一致性与并发度的需求是较通常的作法。但MySQL也提供了主动加锁的机制,使得在较低的隔离级别下,通过加锁,以实现更高级别的一致性。 MySQL的select语句支持for share和for update短语,分别表示对表加共享(Share)锁和写(write)锁,共享锁也叫读锁,写锁又叫排它锁。 下面这条语句,会对表t1加共享锁: select * from t1 for share; 如果select语句涉及多张表,还可分别对不同的表加不同的锁,比如: select * from t1,t2 for share of t1 for update of t2; 加锁短语总是select语句的最后一个短语(复杂的select语句可能有where,group by, having, order by等短语);不管share还是update锁,都是在事务结束时才释放。当然,锁行为会降低并发度。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:5:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"7.6 可串行化 多个事务并发执行是正确的,当且仅当其结果与按某一次序串行地执行这些事务时的结果相同。两个事务t1,t2并发执行,如果结果与t1→t2串行执行的结果相同,或者与t2→t1串行执行的结果相同,都是正确的(可串行化的)。 如果将事务的隔离级别设置为serializable,则这些事务并发执行,无论怎么调度都会是可串行化的。但这种隔离级别会大大降低并发度,在实践中极小使用。MySQL默认的隔离级别为repeatable read,有的DBMS默认为read committed。 8 介质故障与数据库恢复 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:6:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"8.1 MySQL的恢复机制 和大多数DBMS一样,MySQL利用备份、日志文件实现恢复。 具体理论知识在此不详细介绍。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:7:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"8.2 MySQL的备份与恢复工具 MySQL提供了以下工具: 逻辑备份工具:mysqldump 物理备份工具:mysqlbackup(仅限商用版) 日志工具:mysqlbinlog 还原工具:mysql 管理工具:mysqladmin ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:8:0","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"mysqldump 逻辑备份工具,它产生一系的SQL语句,执行这些语句可以重建原数据库的所有对象和数据。缺省输出是控制台,可以通过重定向符号,将其产生的SQL语句集合存入到某个文件。 mysqldump可以备份服务器上的全部数据库,也可以指定某些数据库,或者一个数据库中的某些表。 mysqldump -h127.0.0.1 -uroot -p123123 [options] --databases db_name –databases 参数用指定数据库名,后面可跟一个或多个数据库的名字,多个数据库名间用空格隔开。 mysqldump命令行工具还可以带若干参数,可选的参数多达几十个,详见官方参考手册。这里只介绍一个: –flush-logs 刷MySQL日志,即重新开始一个日志文件。 重新开始一个新的日志文件,对未来确定哪些日志更有用很有帮助。通常海量备份前的日志文件,其重要性会降低许多,因为有备份在手,除非备份文件出故障,你可能不再需要使用之前的日志文件。 mysqlbackup mysqlbackup是MySQL的物理备份工具,只有付费的商用企业版才有。 mysql mysql是MySQL最重要的客户端管理工具,通常用户都是通过mysql登录到MySQL服务器,进行各种操作。此外,还可以直接通过它执行SQL脚本,还原或创建新库。 mysql -h127.0.0.1 -uroot -p12313 \u003c mydb.sql 这样会直接执行mydb.sql的脚本。通过mysqldump备份出来的脚本文件,可以用该方法直接用来恢复原数据库。 mysqladmin mysqladmin是MySQL服务器的管理工具,一般用于配置服务器,也可以用来创建或删除数据库: mysqladmin [options] command [command-arg] [command [command-arg]] 常用的command(执行命令)有: create db_name 创建数据库 drop db_name 删除数据库 flush-logs 刷日志 flush-tables 刷表,所有表数据写入磁盘盘 kill id,id,… 杀死某些进程 password new_password 修改(登录者的)登录密码 ping 检查服务器是否可用 status 显示服务器状态 variables 显示各配置参数的值。 mysqlbinlog mysqlbinlog是MySQL的日志管理工具。在需要手工介入的故障恢复中,该工具必不可少。当然,平常也可以用它查看日志。 mysqlbinlog mysql-bin.000983 上面的例子,用来查看日志文件mysql-bin.000983。MySQL的日志文件具有相同的前缀,后面的数字是日志文件的顺序。这个前缀是可配置的。比如,也可能是binlog.*。 执行日志文件会导致日志所记录的事件重新做一遍,这样可以恢复一个给定时间段的数据,恢复的方法如下: mysqlbinlog [option] binlog_files | mysql -u root -p 介质故障的恢复通常需要把最近一次备份后所有的日志文件全部列上。 mysqlbinlog也支持几十个可选的参数,比如: disable-log-bin 在通过日志恢复数据库期间不再写日志 no-defaults 不使用MySQL默认的设置。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/:8:1","tags":["Mysql"],"title":"Mysql数据库基本语法(四)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%9B%9B/"},{"categories":["Mysql"],"content":"5 用户自定义函数 函数其实有多种,比如标量函数(仅返回一个值)和表函数(返回结果是表),语法也各不相同。这里,我们仅给出一个简化的创建标量函数的语法: create function function_name([para data_type[,...]]) returns data_type begin function_body; return expression; end function_name:函数名; para:参数名; data_type:参数的数据类型; 一个函数可以没有参数,也可以有多个。多参数间用逗号分隔。 function_body:函数体。即由合法的SQL语句组成的程序段。 expression:函数返回值,可以是常量、表达式,甚至是一条select语句查询的值(必须保证结果唯一);该值类型应与returns短语定义的类型相同。 函数一旦定义,就可以像内部函数一样使用,比如出现在select列表、表达式、以及where子句的条件中。 MySQL的函数定义与存储过程的定义一样,在定义函数之前要用“delimiter 界符”语句指定函数定义的结束界符,并在函数定义后,再次使用“delimiter 界符”语句恢复MySQL语句的界符(分号)。 6 安全性控制 与大多数商用DBMS一样,MySQL采用自主存取控制(DAC)机制进行安全性管理。通过用户,数据对象,权限,授权,收回权限等要素进行存取控制。另外,为了方便批量授权给同一类用户,引入了角色。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/:0:0","tags":["Mysql"],"title":"Mysql数据库基本语法(三)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["Mysql"],"content":"6.1 用户(User) MySQL创建用户的语句: create user 用户名 identified by 用户登录密码; 通常用户名可包含域名,限定用户在该域名内登录再有效。例: CREATE USER 'jeffrey'@'localhost' IDENTIFIED BY 'password'; 该语句创建用户jeffrey,密码为’password',仅限在MySQL服务器本机上登录才有效。用户名与域合起来,被称为账户(account)。 注意不要写成:‘jeffrey@localhost’,它代表账户: ‘jeffrey@localhost’@'%' 意即用户名为jefrrey@localhost,在任何机器上登录都有效。两者的含义完全不同。 drop user语句可删除用户。用户被删除时,该用户拥有的权限自动被收回。 alter user语句可重置用户密码: ALTER USER user IDENTIFIED BY 'new_password'; MySQL在安装时,初始用户名为root,此为系统管理员用户,其余用户均由root创建,并授权。经授权的用户也可以创建用户。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/:1:0","tags":["Mysql"],"title":"Mysql数据库基本语法(三)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["Mysql"],"content":"6.2 权限 MySQL常用的权限有: all: 所有权限(grant option除外) alter: alter table权限 alter routine: alter 存储过程 create: create database/table create role: create role create foutine: create 存储过程和函数 create user: create/alter/rename/drop user create view: create view delete: delete语句 drop: drop database/table drop role: drop role execute: 调用存储过程或函数 index:create/drop index insert: insert语句 select: select语句 trigger: 触发器相关操作 update: update语句 等。 select,update,insert,delete还可以用在列上,如select(c_id),update(b_balance)等。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/:2:0","tags":["Mysql"],"title":"Mysql数据库基本语法(三)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["Mysql"],"content":"6.3 角色 角色是权限的集合。如果有一组人(承担相同职责的小组,或者説小组成员扮演相同的角色)应该被授予一组相同的权限,不妨创建一个角色,将那组权限授予该角色,然后再将角色授予该组的每个成员。这比一个个地给每个组员授予一批权限要方便得多。 创建角色的语句: CREATE ROLE [IF NOT EXISTS] role [, role ] ... 一次可以创建多个角色。 删除角色: DROP ROLE [IF EXISTS] role [, role ] ... 角色被删除后,拥有该角色的用户立即失去角色定义的权限组合。不过,如果用户同时拥有多个角色,两个角色代表的权限集合如果有交集,则该用户仍拥有交集代表的权限。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/:3:0","tags":["Mysql"],"title":"Mysql数据库基本语法(三)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["Mysql"],"content":"6.4 GRANT授权语句 以下语句授予权限给用户或角色: grant 权限[,权限] ... on 数据库对象 to user|role,[user|role]... [with grant option] 可以同时将多个权限授予多个用户或角色。 with grant option表示被授权用户可以传播权限,即授权该用户将其拥有的权限(之前获得的权限,通过本语句获得的权限,以及今后获得的权限)再授予其它用户。 以下语句授予角色所代表的权限集给用户或角色: GRANT role [, role] ... TO user_or_role [, user_or_role] ... [WITH ADMIN OPTION] 总之,GRANT语句可以将权限或角色(权限集合)授予用户或角色。但是不能将权限和角色混合授予用户(或角色)。不过,你可以分开用两条不同的GRANT语句来实现:直接授权语句有关键词ON,间接授权(角色代表的权限集合)语句不带ON关键词。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/:4:0","tags":["Mysql"],"title":"Mysql数据库基本语法(三)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["Mysql"],"content":"6.5 REVOKE收回权限语句 以下语句将对象的权限从用户或角色手中收回: revoke 权限[,权限]... on 数据库对象 from user|role[,user|role]... 下列语句把role所代表的权限集合从用户或角色中收回: REVOKE role [, role ] ... FROM user_or_role [, user_or_role ] ... 如果用户本身拥有多个角色所代表的权限集合,而这些集合存在交集,收回其中部分角色代表的权限集后,用户可能仍拥有那个角色所代表的部分权限(交集代表的那部分权限)。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/:5:0","tags":["Mysql"],"title":"Mysql数据库基本语法(三)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%89/"},{"categories":["Mysql"],"content":"3存储过程与事务 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:0:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"3.1 使用流程控制语句的存储过程 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"变量的定义与赋值 用declare语句定义变量,并赋予默认值或初始值,未赋默认值则初始值为null: DECLARE var_name [, var_name] ... type [DEFAULT value] 用set语句给变量赋值,set语句还可以设置许多MySQL的配置参数。 SET variable = expr [, variable = expr] 通过select语句给变量赋值,select语句可以带复杂的where,group by,having等短语。 select col into var_name from table; #将table表中的col列值赋给变量 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:1","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"复合语句与流程控制语句 复合语句BEGIN…END BEGIN [statement_list] END; if语句 IF search_condition THEN statement_list [ELSEIF search_condition THEN statement_list] ... [ELSE statement_list] END IF; while语句 WHILE search_condition DO statement_list END WHILE; ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:2","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"存储过程的定义 存储过程是一种在数据库中存储复杂程序,以便外部程序调用的一种数据库对象。 存储过程是为了完成特定功能的 SQL 语句集,经编译创建并保存在数据库中,用户可通过指定存储过程的名字并给定参数(需要时)来调用执行。 存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用,即具有名字的一段代码,用来完成一个特定的功能。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:3","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"存储过程的创建和查询 创建存储过程: create procedure 存储过程名(参数) 每个存储的程序都包含一个由 SQL 语句组成的主体。此语句可能是由以分号(;)字符分隔的多个语句组成的复合语句。例如: CREATE PROCEDURE proc1() BEGIN SELECT * FROM user; END; MySQL 本身将分号识别为语句分隔符,因此必须临时重新定义分隔符以使 MySQL 将整个存储的程序定义传递给服务器。 要重新定义 MySQL 分隔符,请使用 delimiter命令。使用 delimiter 首先将结束符定义为//,完成创建存储过程后,使用//表示结束,然后将分隔符重新设置为分号(;): DELIMITER // CREATE PROCEDURE proc1() BEGIN SELECT * FROM user; END // DELIMITER ; /也可以换成其他符号,例如$; 执行存储过程: call 存储过程名 存储过程的参数: IN:输入参数,也是默认模式,表示该参数的值必须在调用存储过程时指定,在存储过程中修改该参数的值不能被返回; OUT:输出参数,该值可在存储过程内部被改变,并可返回; INOUT:输入输出参数,调用时指定,并且可被改变和返回。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:4","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"存储过程的查询和删除 查询存储过程: SHOW PROCEDURE STATUS WHERE db='数据库名'; 查看存储过程的详细定义信息: SHOW CREATE PROCEDURE 数据库.存储过程名; 删除存储过程: DROP PROCEDURE [IF EXISTS] 数据库名.存储过程名; ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:1:5","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"3.2 使用游标的存储过程 SQL操作都是面向集合的,即操作的对象以及运算的结果均为集合,但有时候,我们需要一行一行地处理数据,这就需要用到游标(CURSOR),它相当于一个存储于内存的带有指针的表,每次可以存取指针指向的一行数据,并将指针向前推进一行。游标的数据通常是一条查询语句的结果。对游标的操作一般要用循环语句,遍历游标的每一行数据,并将该行数据读至变量,再根据变量的值进行所需要的其它操作。 游标的特点: 不可滚动。即只能从前往后遍历游标数据(即从第1行到最后一行),不能反向遍历,不能跳跃遍历,不能直接访问中间的某一行。 只读。游标里的数据只能读取,不能修改。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:2:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"游标的定义与使用 1.DECLARE语句 DECLARE定义的顺序要求:变量→右边→特情处理 变量用来存储从游标读取的数据,根据编程逻辑的需要,可能还要定义其它变量;游标用来存储SELECT语句读取的数据集;当某些特定情形出现时,会自动触发对应的特情处理程序。 定义变量: DECLARE var_name [, var_name] ... type [DEFAULT value] 定义游标: DECLARE cursor_name CURSOR FOR select_statement 任何合法的select语句(不能带INTO短语),都可以定义成游标。此后可用FETCH语句读取这个select语句查询到的数据集中的一行数据。 注意游标必须定义在变量之后,特情处理程序之前。 一个存储过程可义定义多个游标,但不能同名。 定义特情处理的例子: DECLARE handler_action HANDLER FOR condition_value [, condition_value] ... statement handler_action: { CONTINUE | EXIT } condition_value: { mysql_error_code | SQLSTATE [VALUE] sqlstate_value | condition_name | SQLWARNING | NOT FOUND | SQLEXCEPTION } 游标应用中至少需要定义一个NOT FOUND的HANDLER(处理例程): DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1; 其含义是当抛出NOT FOUND异常时,置变量finished的值为1,程序继续运行。当然,在此之前,应当先定义变量finished,并初始化为0(也可在循环语句之前初始化为0),finished作为循环的控制变量,仅当finished变成1时,循环结束。 如果特情处理例程由多条语句组成,可以用BEGIN…END组成复合语句。 当一个存储过程中存在多个游标时,对任何一个游标的读取(FETCH)都可能会触发特情处理。比如一个游标的数据被遍历完毕,再试图FETCH下一行时,会触发NOT FOUND HANDLER, 并进而改变某个变量的值,但另一个游标中可能还有未处理完的数据。编程者应当自己想办法区分是哪个游标的数据处理完毕。 2 OPEN语句 OPEN cursor_name 该语句打开之前定义的游标,并初始化指向数据行的指针(接下来的第一条FETCH语句将试图读取游标的第1行数据)。 3 FETCH语句 FETCH [[NEXT] FROM] cursor_name INTO var_name [, var_name] ... ETCH语句读取游标的一行数据到变量列表,并推进游标的指针.关键词NEXT, FROM都可省略(或仅省略NEXT)。注意INTO后的变量列表应当与游标定义中的SELECT列表一一对应(变量个数与SELECT列表个数完全相同,数据类型完全一致,每个变量的取值按SELECT列表顺序一一对应)。 FETCH一个未打开的游标会出错。 4 CLOSE语句 CLOSE cursor_name Close语句关闭先前打开的游标,试图关闭一个未曾打开(OPEN)的游标会出错。 没有CLOSE的游标,在其定义的BEGIN…END语句块结束时,将自动CLOSE。 使用游标编写存储过程sp_cursor_demo计算Liverpool足球队在主场获胜的比赛中,上半场的平均进球数,结果通过参数传递。示例程序如下: DELIMITER $$ CREATE PROCEDURE sp_cursor_demo(INOUT average_goals FLOAT) BEGIN DECLARE done INT DEFAULT FALSE; DECLARE matches int DEFAULT(0); DECLARE goals int DEFAULT(0); DECLARE half_time_goals INT; DECLARE team_cursor CURSOR FOR SELECT HTHG FROM epl WHERE (home_team = 'Liverpool') and (ftr = 'H'); DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; OPEN team_cursor; FETCH team_cursor INTO half_time_goals; WHILE NOT DONE DO SET goals = goals + half_time_goals; SET matches = matches + 1; FETCH team_cursor INTO half_time_goals; END while; SET average_goals = goals / matches; CLOSE team_cursor; END $$ DELIMITER; 存储过程定义后,可通过以下语句定义参数,调用过程,再从返回参数中获取结果: SET @average_goals = 0.0; CALL sp_cursor_demo(@average_goals); SELECT @average_goals; 上述带前缀@的变量属于MySQL的用户自定义变量,只在该用户的会话期内有效,对别的用户(客户端)不可见。@前缀变量不用申明变量类型,初始化时,由其值决定其类型。 一般来说,仅当你需要遍历一个数据集,且一次只能处理其中的一行数据时(比如对每一行,要作不同的业务处理),你才需要使用游标。当游标的数据集较大时,会造成较大的网络时延。使用游标时,应尽可能缩小数据规模(去掉不必要的行和列)。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:2:1","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"3.3 使用事务的存储过程 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:3:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"事务的定义和应用 开启事务: START TRANSACTION 或 BEGIN (前者兼容性更好) 事务提交: COMMIT 事务回滚: ROLLBACK 开启或关闭当前会话的自动事务模式: SET autocommit = ON|OFF 也可用1|0,true|false代替ON|OFF。 缺省情况下,autocommit模式被设置为ON,即你在命令行提交的每一条语句会自动封装成一个事务,即使下一条语句发生错误,前一条语句产生的结果也不可撤销。 注意,事务内部不允许嵌套另一个事务,尽量不要在事务内部使用DDL语句,因为即使事务回滚,DDL语句对数据库的修改也不会撤销。 4 触发器 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:3:1","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"4.1 触发器 触发器是与某个表绑定的命名存储对象,与存储过程一样,它由一组语句组成,当这个表上发生某个操作(insert,delete,update)时,触发器被触发执行。触发器一般用于实现业务完整性规则。当primary key,foreigh key, check等约束都无法实现某个复杂的业务规则时,可以考虑用触发器来实现。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:4:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"4.2 触发器的创建 创建触发器的语句: CREATE TRIGGER trigger_name trigger_time trigger_event ON tbl_name FOR EACH ROW trigger_body trigger_nme: 每个触发器有一个唯一的命名 trigger_time: 触发的时机,二选一: BEFORE | AFTER trigger_event: 触发事件,三选一: INSERT | UPDATE | DELETE tbl_name: 与触发器绑定的表 trigger_body: 触发器程序体,可由变量定义、赋值,流程控制,SQL语句等组成。但触发器体内不能使用create,alter,drop等DDL语句,也不能使用start transaction, commit,rollback等事务相关语句。 与创建存储过程、函数一样,创建触发器时也要用delimiter语句重新指定触发器定义语句的界符(触发器内语句的分隔符仍为分号),在触发器定义之后,再把界符更改回去。 before与after触发器的区别: before触发器在试图激活触发器的那条语句(insert|delete|update)之前执行。 after触发器仅在before触发器(如果有的话)和试图激活触发器的那条语句都成功执行后才执行。 before触发器或after触发器如果未能成功执行,则激活触发器的语句也不会执行。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:5:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"4.3 触发器内的特殊表 在触发器内可以使用两类特殊表: old表和new表。它总是与触发器绑定的表有相同的结构,且只能在触发器内访问。 delete触发器可以访问old表,其内容为被delete掉的数据。 insert触发器可以访问new表,其内容为insert的新数据。 update触发器可以访问old表和new表,old表保存着修改前的数据,new表保存着修改后的内容。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/:6:0","tags":["Mysql"],"title":"Mysql数据库基本语法(二)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%8C/"},{"categories":["Mysql"],"content":"1 数据库、表与完整性约束的定义 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:0:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"1.1 创建数据库 进入mysql: mysql -h127.0.0.1 -uroot -p 创建数据库: CREATE DATABASE dbname; 指明访问的数据库: use dbname; ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:1:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"1.2 创建表及表的主码约束 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:2:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"建表语法 CREATE TABLE为保留字,其语义为创建表对象; IF NOT EXISTS为可选短语,其语义为仅当该表不存在时才创建表;如果不带该短语,创建表时,如果同名表已存在,则输出报错信息; tbl_name为表的名字; (列定义|表约束,…)表示表的其它定义都写在一对括号里,括号里为一个或多个“列定义”或“表约束”,如果有多个列的定义或表约束,则它们之间用逗号隔开。 CREATE TABLE [IF NOT EXISTS] tbl_name (列定义|表约束,...) ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:2:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"列定义语法 [NOT NULL |NULL]表示空或非空约束,缺省为NULL,即该列的内容允许为空值,NOT NULL则约束该列的内容必须为非空; DEFAULT关键字为列指定缺省值,可以是常量,也可以是表达式; AUTO_INCREMENT指定该列为自增列(如1,2,3,…),一般用于自动编号,显然只有数字类型的列才可以定义这一特性; [UNIQUE]指定该列值具有唯一性(但可以有空值-甚至多个空值的存在,如果该列没有定义NOT NULL约束); PRIMARY KEY指定该列为主码,相当于定义表的实体完整性约束;只有当主码由单属性组成时,才可以这样定义主码(主码由多属性组成时,应当用表约束来定义); COMMENT用来给列附加一条注释; “REFERENCES”短语为该列定义参照完整性约束,指出该列引用哪个表的哪一列的值,以及违背参照完整性后的具体处理规则(多个规则中选一),具体内容将在随后的练习里再讲解; CHECK(表达式)为列指定“自定义约束”,只有使(表达式)的值为true的数据才允许写入数据库;关键词CONSTRAINT用来为约束命名。 列定义 ::= 列名 数据类型 [NOT NULL | NULL] [DEFAULT {常量 | (表达式)} ] [AUTO_INCREMENT] [UNIQUE [KEY]] [PRIMARY KEY] [COMMENT '列备注'] [REFERENCES tbl_name (col_name) [ON DELETE RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT] [ON UPDATE RESTRICT|CASCADE|SET NULL| NO ACTION|SET DEFAULT]] [[CONSTRAINT [约束名]] CHECK (表达式)] ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:2:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"表约束语法 主码约束以“PK_”打头,后跟表名,一个表只会有一个主码约束; 外码约束以“FK_”打头,后跟表名及列名; CHECK约束以“CK_”打头,后跟表名及列名。 表约束 ::= [CONSTRAINT [约束名]] | PRIMARY KEY (key_part,...) | UNIQUE (key_part,...) | FOREIGN KEY (col_name,...) REFERENCES tbl_name (col_name,...) [ON DELETE RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT] [ON UPDATE RESTRICT|CASCADE|SET NULL| NO ACTION|SET DEFAULT] | CHECK (表达式) 主码约束及唯一性约束中“key_part”的语法规则如下: key_part::= {列名| (表达式)} [ASC | DESC] ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:2:3","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"定义主码 单属性主码,既可在列定义里用PRIMARY KEY约束指定主码,也可以作为表约束单独定义; 组合属性作主码时,该主码只能定义为表约束。 表创建好之后可以使用如下语句列出所有的表: show tables; 还可以使用如下语句查看表的结构,用来检查所建的表是否正确体现了原意: DESC 表名; ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:2:4","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"1.3 创建外码约束 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:3:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"外码 外码是表中的一个或一组字段(属性),它可以不是本表的主码,但它与某个主码(同一表或其它表的主码)具有对应关系(语义完全相同)。外码可以是一列或多列,一个表可以有一个或多个外码。当我们谈论外码时,一定有个主码与它对应,外码不可能单独存在。主码所在的表为主表,又称父表,外码所在的表为从表,又称子表。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:3:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"外码约束 外码用来在数据之间(即外码与其对应的主码间)建立关联。参照完整性约束用于约束外码列的取值范围:外码列的取值要么为空,要么等于其对应的主码列的某个取值。在语义允许,又不违反其它约束规则的情形下,外码列的取值才可以为空。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:3:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"定义外码约束 可在定义表的同时定义各种完整性约束规则(当然包括外码约束,亦即参照完整性约束)。外码约束既可以定义为列约束,亦可定义为表约束。 列级外码约束的语法格式如下: 列级外码约束 ::= 列名 数据类型 [REFERENCES tbl_name (col_name) [ON DELETE RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT] [ON UPDATE RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT] 表约外码约束的语法格式如下: 表级外码约束 ::= [CONSTRAINT [约束名]] FOREIGN KEY (col_name,...) REFERENCES tbl_name (col_name,...) [ON DELETE RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT] [ON UPDATE RESTRICT|CASCADE|SET NULL| NO ACTION|SET DEFAULT] MySQL表级外码约束的好处是可以给约束命名,且支持多属性组合外码(即外码由多个列组成)。**事实上,外码约束定义在表一级,是不二的选择,因为MySQL对列级外码约束的支持仅停留在语法检查阶段,实际并没有实现(至少8.0.22还没有实现)。**外码约束的名称一般以“FK_”为前缀,这是约定俗成的规则。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:3:3","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"1.4 check约束 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:4:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"用户定义的完整性约束 关系数据库的完整性约束共有三类:实体完整性约束,参照完整性约束以及用户定义的完整性约束。实体完整性约束和参照完整性约束分别用PRIMARY KEY和FOREIGN KEY来实现;CHECK约束是最主要的一类用户定义的完整性约束,用于定义用户对表中的某列的数据约束,或表中一行中几列之间应该满足的完整性约束。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:4:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"CHECK约束的定义方法 如果约束仅涉及单个列,则该约束既可以定义为列约束,也可以定义为表约束,例如:“性别”列的取值仅限从(“男”,“女”)中取值; 如果约束涉及表的多个列,则该约束只能定义为表约束,例如:如果职称为“教授”,则它的薪资应当不低于6000元。这个约束涉及到“职称”和“薪资”两个列的内容,故只能用表约束来实现。 CHECK约束的语法: CHECK约束 ::= [CONSTRAINT [约束名]] CHECK (条件表达式)] 只有当条件表达式的值为true时,数据(插入的新数据,或修改后的数据)才会被接受,否则将被拒绝。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:4:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"1.5 DEFAULT约束 默认值约束(Default约束)用于给表中的字段指定默认值,即往表里插入一条新记录时,如果没有给这个字段赋值,那么DBMS就会自动赋于这个字段默认值。 Default约束只能定义为列一级约束,即在需要指定默认值的列之后用关键字DEFAULT申明默认值,其语法为: col_name data_type [DEFAULT {literal | (expr)} ] 即在列名与列的数据类型之后申明Default约束。当然Default约束只是众多列约束中的一种,该列可能还有NOT NULL, UNIQUE, AUTO_INCREMENT, CHECK,FOREIGN KEY等其它约束。 DEFAULT关键字引出的默认值可以是常量,也可以是一个表达式。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:5:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"举例 AUTO_INCREMENT约束仅用于整数列; DEFAULT约束指定默认值为表达式时,表达式要写在一对括弧里; 这里,curdate()是MySQL的系统函数,其功能是取当前日期; 语句中,表名称order前后的符号是必须的,因为order是MySQL的关键字,当表名或列名与关键字冲突时,名称前后必须加`号。 create table `order`( orderNo int auto_increment primary key, orderDate date default (curdate()), customerNo char(10), employeeNo char(10)); ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:5:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"1.6 UNIQUE约束 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:6:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"UNIQUE约束 跟主码(Primary Key)约束一样,Unique约束既可以是对单属性的约束,也可以是对属性组约束,具有Unique约束的属性或属性组的取值必须是唯一的,否则就违反了Unique约束。不过,跟主码不同的是,Unique约束并不要求字段必须非空(Not Null),所以,实际上,它只能约束非空的属性(组)取值是唯一的。同时具有Not Null约束的Unique属性(组)相当于候选码。一个表只能定义一个主码约束,但可以定义多个Unique约束。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:6:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"UNIQUE约束的语法 跟主码约束一样,单字段的Unique约束既可定义为列约束,亦可定义为表约束,组合字段的Unique约束只能定义为表约束。 Unique列约束的语法为: col_name data_type UNIQUE Unique表约束的语法为: [CONSTRAINT [约束名]] UNIQUE(列1, 列2, ...) Constraint短语可以省略。既使写上关键词constraint,也可以省略约束名。约束未命名时,MySQL将按一定规则自动予以命名。 NOT NULL只能作列约束,且不用命名。UNIQUE约束作列约束时不能自主命名,作表约束时可以自主命名。 2 表结构与完整性约束的修改 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:6:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"2.1 修改表名 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:7:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"ALTER TABLE语句 Alter Table语句用于修改由Create Table语句创建的表的结构。比如,添加或删除列,添加或删除约束,创建或销毁索引,更改列的数据类型,更改列名甚至表名等。 ALTER TABLE 表名 [修改事项 [, 修改事项] ...] 常用修改事项有: 用ADD关键词添加列和约束(主码、外码、CHECK、UNIQUE等约束); 用DROP关键词删除列、约束和索引(含Unique); 用MODIFY关键词修改列的定义(数据类型和约束); 用RENAME关键词修改列、索引和表的名称; 用CHANGE关键词修改列的名称,同时还可以修改其定义(类型和约束)。 修改事项 ::= ADD [COLUMN] 列名 数据类型 [列约束] [FIRST | AFTER col_name] | ADD {INDEX|KEY} [索引名] [类型] (列1,...) | ADD [CONSTRAINT [约束名]] 主码约束 | ADD [CONSTRAINT [约束名]] UNIQUE约束 | ADD [CONSTRAINT [约束名]] 外码约束 | ADD [CONSTRAINT [约束名]] CHECK约束 | DROP {CHECK|CONSTRAINT} 约束名 | ALTER [COLUMN] 列名 {SET DEFAULT {常量 | (表达式)} | DROP DEFAULT} | CHANGE [COLUMN] 列名 新列名 数据类型 [列约束] [FIRST | AFTER col_name] | DROP [COLUMN] 列名 | DROP {INDEX|KEY} 索引名 | DROP PRIMARY KEY | DROP FOREIGN KEY fk_symbol | MODIFY [COLUMN] 列名 数据类型 [列约束] [FIRST | AFTER col_name] | RENAME COLUMN 列名 TO 新列名 | RENAME {INDEX|KEY} 索引名 TO 新索引名 | RENAME [TO|AS] 新表名 说明: 注意RENAME,MODIFY和CHANGE的区别:仅改列名,用RENAME; 只改数据类型不改名,用MODIFY; 既改名又改数据类型,用CHANGE。 在用MODIFY,CHANGE更改列的数据类型和约束时,修改后的CHECK约束并不会生效(MySQL只作语法检查,并未实现代码–至少MySQL 8.0.22还未实现)。但用ADD新增列的CHECK约束,是有效的。 删除主码约束只能用Drop Primary Key短语,不能使用drop constraint短语,即便在创建主码约束时显式命名了该主码约束。试图使用“drop constraint 主码约束名”短语删除主码,会给出错误提示,显示该约束并不存在。因为MySQL并没有完全实现“constraint 约束名 primary key(…)”短语的功能,仅作了语法检查,然后直接忽略了主码约束的命名。 给已有列增加Default约束,可用 alter 列 set default ... 短语;删除列的default约束,可用 alter 列 drop default 短语。当然,也可以用 Modify 列名 数据类型 ... 短语。如果该短语没有default约束,就相当于删除了原来的default约束,如果该短语带有default约束,就相当于添加了default约束,如果之前已有default约束,则新的Default约束将代替原有的Default约束; 删除unique约束,既可用 drop constraint 约束名 短语,也可以用 drop key 索引名 短语来实现,唯一性(unique)约束实际是用Unique索引来实现的,Unique索引的名字总是与Unique约束名完全一样,它们本就是同一套机制。如果没有显式命名的话,Unqiue索引名或者说Unique约束名一般与列同名(组合属性作索引,则与组合属性中的第1列同名)。但要注意是的,在更改列名后,Unique索引名并不会随之更改。在创建Unqiue约束时,用“constriant”短语给约束取一个有意义的名字,是一个值得推荐的习惯。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:7:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"更改表名 alter table 表名 rename [TO|AS] 新表名 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:7:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"2.2 添加与删除字段 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:8:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"给表添加字段 关键字FIRST指示新添加的列为第1列; AFTER指示新添加的列紧跟在指定列的后面。 如果省略位置指示,则新添加的列将成为表的最后一列。 关键字column可以省略。 ALTER TABLE 表名 ADD [COLUMN] 列名 数据类型 [列约束] [FIRST | AFTER 列名] 举个例子: alter table student add mobile char(11) constraint CK_student_mobile check(mobile rlike '1[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'); check约束中的rlike还可以用regexp替代,它们是同义语。跟Oracle一样,MySQL用正则匹配表达式来测试字段值是否符合某个pattern,rlike比like关键词所支持的功能要强大得多。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:8:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"删除表中的字段 关键字COLUMN可以省略,其语法格式为: ALTER TABLE 表名 DROP [COLUMN] 列名 举个例子: 在学生档案里记录年龄的作法并不科学,因为年龄会随着时间的变化而变化,档案里记录17岁,还得根据当年记录的日期以及当下的日期推算实际年龄。替代方案是记录出生日期而不是年龄。解决方案: # 第1步:添加列DOB alter table student add DOB date after sex; # 第2步,根据age推算DOB update student set DOB = date_add('2020-9-1', interval -1*age year); # date_add()是mysql的函数 select * from student; # 查看表student的内容 # 第3步,删除列age alter table student drop age; ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:8:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"2.3 修改字段 修改列名、列数据类型和列约束,以及列序的修改事项有: 修改事项 ::= ALTER [COLUMN] 列名 {SET DEFAULT {常量 | (表达式)} | DROP DEFAULT} | CHANGE [COLUMN] 列名 新列名 数据类型 [列约束] [FIRST | AFTER col_name] | MODIFY [COLUMN] 列名 数据类型 [列约束] [FIRST | AFTER col_name] | RENAME COLUMN 列名 TO 新列名 CHANGE短语可修改列名、数据类型和列约束; MODIFY短语可修改列的数据类型和约束; RENAME短语仅用于更改列名; ALTER短语仅用于修改列的DEFAULT约束或删除列的DEFAULT约束。 CHANGE和MODIFY短语还可以修改列在表中的位置。 create database MyDb; use MyDb; create table s( sno char(10) primary key, name varchar(32) not null, ID char(18) unique ); ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:9:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"修改字段名称 注意:关键字COLUMN不能省略 ALTER TABLE 表名 RENAME COLUMN 列名 TO 新列名 如果修改列名的同时,还要修改列的类型和约束,则用CHANGE短语: ALTER TABLE 表名 CHANGE [COLUMN] 列名 新列名 数据类型 [列约束] [FIRST | AFTER col_name] 如果新列带有CHECK约束的话,MySQL只会对这个约束作语法检查,并不会去实现这个约束,其它类型的约束没有问题。如果真有这样的需求,不如先DROP之前的列,再ADD新的列,新列附带的CHECK约束是会被实现的。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:9:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"修改字段的数据类型和约束 如果列名称不变,仅需要修改其数据类型和约束,则用MODIFY短语: ALTER TABLE 表名 MODIFY [COLUMN] 列名 数据类型 [列约束] [FIRST | AFTER col_name] 注意,一旦使用MODIFY短语修改列,则该列之前的数据类型、约束将被新的数据类型和约束取而代之。如果之前定义了列约束,修改后不带列约束,相当于删除了之前的约束。 如果需要修改(或添加)列的DEFAULT约束,则既可用上面的MODIFY短语,也可以使用ALTER短语: ALTER TABLE 表名 ALTER [COLUMN] 列名 SET DEFAULT {常量 | (表达式)} 删除列的DEFAULT约束,则可以使用ALTER短语(或MODIFY短语): ALTER TABLE 表名 ALTER [COLUMN] 列名 DROP DEFAULT ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:9:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"修改字段在表中的位置 如果仅需修改列在表中的位置,仍用MODIFY短语: ALTER TABLE 表名 MODIFY [COLUMN] 列名 数据类型 [列约束] [FIRST | AFTER col_name] 举个例子: alter table resident modify idNo char(18), modify height int unsigned, rename column educationalBackground to education; ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:9:3","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"2.4 添加、删除与修改约束 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:10:0","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"主码的添加与删除 删除主码: ALTER TABLE 表名 DROP PRIMARY KEY; drop index `PRIMARY` on 表名; 添加主码: ALTER TABLE 表名 ADD [CONSTRAINT [约束名]] PRIMARY KEY(列1,列2,...); MySQL尽管在语法上支持主码约束的命名,但实际上并没有真正实现主码约束的命名功能。即,MySQL并不会创建用户语句中所指定的约束名。所以,试图通过约束名删除主码约束是行不通的。 MySQL中,所有的主码约束(主码索引)名均为PRIMARY,无论怎么命名或更命,这个名字都不会改变。由于PRIMARY是MySQL的保留字,所以,在引用这个主码约束(索引)名时,必须用一对``符号将PRIMARY括起来。 举个例子: # 第1步:删除错误的主码定义 alter table score drop primary key; # 第2步:重新创建主码 alter table score add constraint PK_score primary key(sno,cno); alter table score add primary key(sno,cno); # 进阶版 alter table score drop primary key, add primary key(sno,cno); ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:10:1","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"外码的删除与添加 删除外码: ALTER TABLE 表名 DROP CONSTRAINT 约束名 ALTER TABLE 表名 DROP FOREIGN KEY 约束名 添加外码: ALTER TABLE 表名 ADD [CONSTRAINT [约束名]] 外码约束 约束名是可选的,如果省略命名短语,MySQL将按一定的规则自动命名。将来如果要删除该约束,必须先查询到该约束的名字(注:从MySQL的数据字典查询)。 创建外码时,MySQL将同步创建外码索引,如果外码约束有显式命名,则外码索引与外码约束同名。如果外码约束未命名,则外码索引与外码列的列名同名。 删除外码约束时,外码索引不会跟着删除。如果将来重新创建了外码,并显式命名,则外码索引会自动更名(与外码约束名保持相同)。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:10:2","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"CHECK约束的删除与添加 删除check约束: ALTER TABLE 表名 DROP CONSTRAINT 约束名 添加check约束: ALTER TABLE 表名 ADD [CONSTRAINT [约束名]] check(条件表达式) 添加约束时,如果现有数据与该约束规则相矛盾,则创建约束的请求会被拒绝。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:10:3","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["Mysql"],"content":"UNIQUE约束的添加与删除 删除Unique约束: alter table 表名 drop constraint 约束名; drop index 索引名 on 表名; 添加Unique约束: alter table 表名 ADD [CONSTRAINT [约束名]] UNIQUE(列1,...) 创建unique约束时,将同步创建unique索引,索引名与约束同名。如果未显式命名unique约束或索引,MySQL将按一定规则自动命名(单列的unique索引或约束与列同名)。 约束的修改一般通过先删除旧约束再重建新约束来实现。 ","date":"2022-12-07","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/:10:4","tags":["Mysql"],"title":"Mysql数据库基本语法(一)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%B8%80/"},{"categories":["计算机网络"],"content":"最近在复习计算机网络考试,于是按照《计算机网络自顶向下方法》(原书第7版)一书梳理了1-7章的知识,其中第五章的内容合并到第四章中了。 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:0:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["计算机网络"],"content":"1 计算机网络和因特网 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:1:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["计算机网络"],"content":"2 应用层 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:2:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["计算机网络"],"content":"3 运输层 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:3:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["计算机网络"],"content":"4 网络层 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:4:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["计算机网络"],"content":"6 链路层和局域网 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:5:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["计算机网络"],"content":"7 无线网络和局域网 ","date":"2022-12-03","objectID":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/:6:0","tags":["计算机网络"],"title":"计算机网络知识总结","uri":"/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93/"},{"categories":["深度学习"],"content":"本文为论文 Label Embedding Online Hashing for Cross-Modal 的阅读笔记。 论文下载:https://doi.org/10.1145/3394171.3413971 ","date":"2022-11-18","objectID":"/lemon/:0:0","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"1 简介 学习散列,作为最著名的近似近邻搜索技术之一,近年来吸引了很多人的注意。它旨在将高维实值特征映射到紧凑的二进制编码,同时保留原始空间中的相似性。然后,可以用XOR操作在Hamming空间中进行搜索,效率高,存储成本低。 许多跨模态散列方法已经被提出并取得了很好的性能。但大多数现有的方法通过批处理学习二进制代码或哈希函数。即在学习过程前,所有的训练数据都可用。这将产生以下问题: 实际数据通常以流方式收集,有新数据到来时,批处理方法需要对所有数据重新训练 → 效率低 训练集随训练时间变大 → 计算成本高 为了解决这些问题,在线散列被提出,但仍存在问题: 大多数在线散列方法是为单模态检索设计的,很难直接扩展到跨模态检索。少数在线跨模态散列模型被提出,但性能较差,因为异质模态之间的关联性难以捕捉。 只根据新到达的数据更新散列函数,忽略了新旧数据间的相关性 → 丢失现有数据的信息 → 现有在线散列。 新数据到来时,哈希函数可以有效地重新训练,但哈希码必须对所有累积数据重构 → 更新方案低效。 离散优化大多采用松弛策略 → 量化误差大。 为了解决上述问题,这篇文章提出了一种新的监督下的跨模式检索的在线散列方法,即Label EMbedding ONline hashing,简称LEMON。本文的主要贡献总结如下: 提出了一种新的有监督的在线散列检索方法,即LEMON。 它通过一个标签嵌入框架来捕捉语义结构,其中包括标签相似性的保存和标签重构,从而得到更有辨识度的二进制码。 通过优化内积最小化问题将新旧数据的哈希码连接起来,解决了更新不平衡问题。 采用两步学习策略,有效地更新哈希函数和二进制码,同时保持旧数据库的哈希代码不可更改,使其计算复杂度仅与流数据的大小有关。 提出了一种迭代优化算法来离散地解决二进制优化问题,极大地减少 量化误差。 在三个基准数据集上的实验结果表明,LEMON在跨模式检索任务上优于一些先进的离线和在线散列方法,并且可以扩展到大规模数据集。 ","date":"2022-11-18","objectID":"/lemon/:1:0","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"2 相关工作 现有工作存在的问题: 单模态:不能直接用于跨模态检索任务;必须在每一轮更新所有的二进制代码,效率非常低 多模态:不能跨模态检索 跨模态:不能充分利用原始特征、语义标签;不能很好地以流的方式来捕捉数据的相似性信息 单模态:查询和要检索的文档都只有一个模态(图像→图像) 多模态:查询和要检索的文档必须至少有一个模态相同(图像、文本→图像、文本) 跨模态:查询和要检索的文档模态不同(图像→文本) ","date":"2022-11-18","objectID":"/lemon/:2:0","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"3 方法 ","date":"2022-11-18","objectID":"/lemon/:3:0","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"3.1 Notations 假设每个样本由 $l$ 个模态组成。在第 $t$ 轮,一个新的数据块 $\\vec{X}^{(t)}$ 被添加到数据库中。常用变量的说明如下: 符号 意义 $\\vec{X}_m^{(t)}∈R^{d_m×n_t}$ 表示新数据块的第 $m$ 个模态,其中 $n_t$ 是新数据块的大小, $d_m$ 是特征维度。$m∈{1,2,…,l}$ $\\vec{L}^{(t)}∈R^{c×n_t}$ 新数据块的标签矩阵,其中 $c$ 是语义类别的数量 $\\tilde{X}^{(t)}$ 现有数据 $N_{t-1} = \\sum_{k=1}^{t-1} n_k$ 现有数据的大小,$N_t=N_{t-1} +n_t$ $\\tilde{L}^{(t)}∈R^{c×N_{t-1}}$ 现有数据的相应标签矩阵 $X^{(t)}_m=[\\tilde{X}^{(t)}_m,\\vec{X}_m^{(t)}]∈R^{d_m×N_t}$ 代表当前整个数据库 $L^{(t)}=[\\tilde{L}^{(t)},\\vec{L}^{(t)}]∈R^{c×N_t}$ 代表整个标签矩阵 $\\tilde{B}^{(t)}$ 现有数据的哈希码 $\\vec{B}^{(t)}$ 新数据的哈希码 我们的目标是学习所有模态的 $r$ 位统一哈希码$B^{(t)}=[\\tilde{B}^{(t)},\\vec{B}^{(t)}]∈R^{r×N_t}$,和第$m$ 个模态的哈希函数 $H_m^{(t)}(·)$。 本文采用了一个两步学习方案:首先学习现有样本和新数据的哈希码,再基于学习到的哈希码,进一步学习哈希函数。 ","date":"2022-11-18","objectID":"/lemon/:3:1","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"3.2 Hash Codes Learning 算法整体伪代码: 3.2.1 Label Embedding 根据检索任务的目标,二进制代码应该保留实例的语义相似性。为了实现这一点,任务可以定义为以下的内积最小化问题: $$ min_{B(t)} ∥B^{(t)⊤}B^{(t)} − rS^{(t)}∥^2, s.t. B^{(t)} ∈ {−1, 1}^{r×N_t}\\tag{1} $$ $S_{(t)}$ 是语义相似度矩阵。如果第 $i$ 个实例和第 $j$ 个实例至少有一个共同的标签,则 $S^{(t)}_{ij} = 1$ ,否则 $S^{(t)}_{ij} = -1$ 。此方案存在的问题: 存储、计算成本大 不能表明细粒度的语义相似性,特别是对于多标签数据 为了解决上述问题,重新定义相似性矩阵,并通过二进制的哈希码保存。标签相似度矩阵如下: $$ S^{(t)} = 2U^{(t)⊤} U^{(t)} − 11^⊤\\tag{2} $$ 其中 $U^{(t)⊤}$ 是2规范化的标签矩阵,定义为 $u^{(t)}_i =l^{(t)}_i/∥l^{(t)}_i ∥$ ,而 $l^{(t)}_i$ 是 $L^{(t)}$ 的第 $i$ 列。 为了使 $S^{(t)}$ 能够用于在线场景,进一步将其改写为一个块状矩阵。$S_{oo}^{(t)}$,$S_{oc}^{(t)}$,$S_{co}^{(t)}$,$S_{cc}^{(t)}$分别是旧数据的成对相似度矩阵、旧新数据的相似度矩阵、旧新数据的相似度矩阵、新数据的成对相似性矩阵。 我们试图将更多的标签信息嵌入待学习的二进制码中。假设所有样本标签都可以从学习到的二进制码中重构。可以进一步定义以下优化问题: $$ min_{{B,P}^{(t)}} ∥L^{(t)} −P^{(t)}B^{(t)}∥^2+γ ∥P^{(t)}∥^2, s.t. B^{(t)} ∈ {−1, 1}^{r×N_t} \\tag{5} $$ 其中 $γ$ 是惩罚参数,$P^{(t)}∈R^{c×r}$ 是投影矩阵。合并(1)和(5): $$ min_{{B,P}^{(t)}} α ∥B^{(t)⊤}B^{(t)} − rS^{(t)}∥^2+ β∥L^{(t)} −P^{(t)}B^{(t)}∥^2+βγ ∥P^{(t)}∥^2 \\tag{6} $$ 其中 $α$ 和 $β$ 是权衡参数。显然,上述方法通过保留标签的相似性、重建标签,可以将更多的语义信息嵌入二进制码中。此外,它通过最一致的语义标签来匹配异质性的模式,从而产生统一的二进制码,非常适用于在线学习。 3.2.2 Online Learning 理想情况下,我们希望保持 $\\tilde{B}^{(t)}$ 不变,只更新 $\\vec{B}^{(t)}$ 。将(3)代入(6)得: $$ min_{{B,P}^{(t)}} α ∥\\vec{B}^{(t)⊤}\\tilde{B}^{(t)} − rS_{co}^{(t)}∥^2+ α ∥\\tilde{B}^{(t)⊤}\\vec{B}^{(t)} − rS_{oc}^{(t)}∥^2 + α ∥\\vec{B}^{(t)⊤}\\vec{B}^{(t)} − rS_{cc}^{(t)}∥^2 + $$ $$ β∥\\vec{L}^{(t)} −P^{(t)}\\vec{B}^{(t)}∥^2+ β∥\\tilde{L}^{(t)} −P^{(t)}\\tilde{B}^{(t)}∥^2+βγ ∥P^{(t)}∥^2, \\tag{7} $$ 这个在线目标是由最初的批处理目标推导出来的,即公式(6)。包含 $S_{oo}^{(t)}$ 的项被切断,因为 $\\tilde{B}^{(t)}$ 不变。因此,它对传入的数据不太敏感,能够产生更多的鉴别性哈希码。通过上述目标函数,新数据之间的成对相似性被保留。更重要的是,新旧数据之间的关联性也被 $S_{oc}^{(t)}$ 或 $S_{co}^{(t)}$ 捕获。因此,公式(7)能将更多的相似性信息嵌入到二进制码中。 然而随着时间的积累,旧数据库的样本数量比新数据块的大得多,导致相似性矩阵 $S_{co}^{(t)}$ 稀疏且不平衡,大多数元素是负的。使用直接的离散优化可能会带来巨大的信息损失,因为硬二进制矩阵分解可能会偏向于保持不相似的信息而丢失相似的信息。为了解决这个问题,我们用一个实值 $V^{(t)}$ 来代替一个 $B^{(t)}$。类似地,有 $V^{(t)}=[\\tilde{V}^{(t)},\\vec{V}^{(t)}]$ 。为了减少 $B^{(t)}$ 和 $V^{(t)}$ 间的信息损失,引入一个正交旋转矩阵的正则化项:$R^{(t)}∈R^{r×r}$ 。为了使 $V^{(t)}$ 无偏,引入正交和平衡约束。目标函数成为以下函数: $$ min_{{\\vec{B},\\vec{V},P,R}^{(t)}} ∥\\vec{B}^{(t)}−R^{(t)}\\vec{V}^{(t)}∥^2+∥\\tilde{B}^{(t)}−R^{(t)}\\tilde{V}^{(t)}∥^2 + α ∥\\vec{V}^{(t)⊤}\\tilde{B}^{(t)} − rS_{co}^{(t)}∥^2+ $$ $$ α ∥\\tilde{V}^{(t)⊤}\\vec{B}^{(t)} − rS_{oc}^{(t)}∥^2 + α ∥\\vec{V}^{(t)⊤}\\vec{B}^{(t)} − rS_{cc}^{(t)}∥^2 + β∥\\vec{L}^{(t)} −P^{(t)}\\vec{V}^{(t)}∥^2+ $$ $$ β∥\\tilde{L}^{(t)} −P^{(t)}\\tilde{V}^{(t)}∥^2+βγ ∥P^{(t)}∥^2, \\tag{8} $$ 这样一来,二元约束只强加在一个被分解的变量上,而且避免了二元矩阵分解。 此外,实值 $V^{(t)}$ 比 $B^{(t)}$ 能更准确地捕捉语义信息,确保在相似性保存过程中可接受的信息损失,从而解决更新不平衡问题。此外,它仍然保留了离散的约束条件,并通过sign(·)操作有效地生成二进制哈希码。$V^{(t)}$ 在相似性空间和标签空间之间架起了桥梁。 3.2.3 Efficient Discrete Optimization 为了解决公式 (8) 的问题,我们提出了四步迭代优化算法,该算法有效地、不连续地生成哈希码。在每个步骤中,一个变量被更新,而其他变量被固定。 更新 $P^{(t)}$ 。令公式 (8) 关于 $P^{(t)}$ 的导数为零,得出 $P^{(t)}$ : $$ P^{(t)} = C^{(t)}_1 (C^{(t)}_2 + γ I)^{−1} \\tag{9} $$ 更新 $\\vec{V}^{(t)}$ 。当Bfi(t)、P(t)、R(t)固定时,公式(8)可以被简化为: $$ max_{\\vec{V}^{(t)}} tr(Z\\vec{V}^{(t)}), s.t. \\vec{V}^{(t)}\\vec{V}^{(t)T} = n_tI, \\vec{V}^{(t)}1 = 0. \\tag{13} $$ 我们可以发现,方程(13)有一个封闭形式的最优解。记为 $J = I - \\frac{1}{nt} 11^⊤$。注意 $J$ 是实时计算的。然后,该问题可以通过对 ${ZJZ}^⊤$ 进行奇异值分解来解决: $$ {ZJZ}^⊤=\\left[\\begin{matrix}G \u0026 \\hat{G}\\end{matrix}\\right] \\left[\\begin{matrix}Ω \u0026 0 \\ 0 \u0026 0\\end{matrix}\\right] \\left[\\begin{matrix}G \u0026 \\hat{G}\\end{matrix}\\right]\\tag{16} $$ 这里,$Ω∈R^{r^′×r^′}$ 是正特征值的对角矩阵,$G∈R^{r×r^′}$ 是相应的特征向量。$\\hat{G}$ 是剩余的 $r - r^′$ 零特征值的特征向量。$r'$ 是 $ZJZ^⊤$ 的等级。通过对 $\\hat{G}$ 进行Gram-Schmidt处理,可以得到一个正交矩阵 $\\bar{G}∈R^{r×(r-r^′)}$。我们进一步表示 $Q = JZ^⊤GΩ^{-1/2}$,并生成一个随机的正交矩阵 $\\bar{Q}∈R^{n_t×(r-r^′)}$ 。如果 r′=r,$\\bar{G}$ 和 $\\bar{Q}$ 为空。则公式(13)的最优解是: $$ \\vec{V}^{(t)} = \\sqrt{n_t} \\left[\\begin{matrix}G \u0026 \\bar{G}\\end{matrix}\\right] \\left[\\begin{matrix}Q \u0026 \\bar{Q}\\end{matrix}\\right]^T.\\tag{17} $$ 更新 $\\vec{R}^{(t)}$ 。在除 $\\vec{R}^{(t)}$ 之外的所有变量固定的情况下,公式(8)变成经典的正交普鲁斯特问题,可以通过重度分解来解决: $$ C^{(t)}_5 = AΣ\\hat{A}^⊤,\\tag{18} $$ $$ C^{(t)}_5 = C^{(t-1)}_5+\\vec{B}^{(t)}\\vec{V}^{(t)T}, C^{(t-1)}_5=\\tilde{B}^{(t)}\\tilde{V}^{(t)T} .\\tag{19} $$ 得到 $R^{(t)}$ 的最佳解为: $$ R^{(t)} = A\\","date":"2022-11-18","objectID":"/lemon/:3:2","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"3.3 Hash Functions Learning 获得统一的二进制码后,需要学习哈希函数来适应多种模式。为了达到这个目的,可以采用不同的模型,如线性回归、支持向量机、深度神经网络。我们在学到的二进制码的监督下,为每种模式训练一个线性回归模型。具体来说,给定学习的二进制码 $B^{(t)}$ 和第 $m$ 个模态的特征矩阵 $X^{(t)}_m$,线性映射模型可以通过解决以下问题被学习: $$ min_{W^{(t)}_m} ∥B^{(t)} − W^{(t)}_m X^{(t)}_m∥^2 + ξ ∥W^{(t)}_m∥^2,\\tag{24} $$ 其中 $ξ$ 是一个惩罚参数,$W^{(t)}_m$ 是映射矩阵。通过将公式 (25) 关于 $W^{(t)}_m$ 的导数设为零,得到最佳解: $$ W^{(t)}_m = H^{(t)}_m(F^{(t)}_m + ξ I)^{−1},\\tag{26} $$ $$ H^{(t)}_m = H^{(t-1)}_m+\\vec{B}^{(t)}\\vec{X}^{(t)T}_m, H^{(t-1)}_m=\\tilde{B}^{(t)}\\tilde{X}^{(t)T}_m, $$ $$ F^{(t)}_m = F^{(t-1)}_m+\\vec{X}^{(t)}\\vec{X}^{(t)T}_m, F^{(t-1)}_m=\\tilde{X}^{(t)}\\tilde{X}^{(t)T}_m .\\tag{27} $$ 此后,给定一个新的查询,我们可以取第 $m$ 个模态 $x_m∈R^{d_m}$,并通过以下哈希函数生成其哈希码: $$ H^{(t)}_m (x_m) = sign(W^{(t)}_mx_m).\\tag{28} $$ 训练过程的整体计算复杂度与新数据的大小 $n_t$ 呈线性关系,而与旧数据库的大小无关。因此,LEMON可以扩展到大规模的在线检索任务。 ","date":"2022-11-18","objectID":"/lemon/:3:3","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"4 实验 为了评估LEMON的性能,我们在三个基准数据集上进行了广泛实验。 ","date":"2022-11-18","objectID":"/lemon/:4:0","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"4.1 实施细节 在实现LEMON的过程中,我们首先对MIRFlickr-25K数据集进行了参数敏感性分析,当α和β为 1e4 时,LEMON取得最佳效果。此外我们观察到,参数对性能的影响并不显著。因此,为了简单起见,所有的数据集上都设置了相同的参数,即α=β=1e4。根据经验,$γ$ 和 $ξ$ 分别被设定为0.1和1。每一轮的迭代次数为5。 在实验中,我们进行了两种类型的跨模式检索任务。图像→文本和文本→图像。利用平均精度(MAP)和训练时间,来评估所有方法的性能。 ","date":"2022-11-18","objectID":"/lemon/:4:1","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["深度学习"],"content":"4.2 结果分析 4.2.1 平均精度分析 MIRFlickr-25K数据集的结果如表1所示,展示了不同长度样例的MAP值。 不同方法的性能如下图所示: 可以得出: LEMON在所有情况下都持续优于其他方法,表明其在处理跨模式检索任务方面的有效性。 一般来说,离线基线(如DLFH和SCRATCH),比在线基线(如OCMH和OLSH)表现更好。 从图1中可以看到,LEMON实现了持续的性能提高,证明了LEMON可以通过新旧数据库之间的相关性,将更多的语义信息嵌入二进制码。 随着轮次的增加,大多数离线方法的性能都在下降。最有可能的原因是它们在每轮中用所有累积的样本重新训练哈希函数和哈希代码。 大多数方法随着哈希码长度的增加而表现得更好,表明更长的比特可以携带更多的鉴别信息。 大多数方法在文本→图像的任务中比图像→文本的任务中表现得更好,可能的原因是,文本特征可以更好地描述。 其他两个数据集的结果与其类似。 4.2.2 时间成本分析 根据之前的分析知,LEMON的复杂性与新数据的大小呈线性关系。由图四可知,LEMON在所有情况下都是最快的。离线方法需要更长的训练时间,并且时间成本随着回合数的增加而显著增加,因为它们必须对所有累积的数据重新训练哈希函数,这使得它们在在线情况下效率很低;而在线方法的训练时间不会。因此LEMON的训练非常有效,并且可以扩展到大规模在线检索。 4.2.3 参数敏感度分析 我们进行了实验来分析包括α和β在内的参数的敏感性。由图5可以看到,这些参数确实对LEMON的性能有一些影响,但并不明显。 ","date":"2022-11-18","objectID":"/lemon/:4:2","tags":["深度学习"],"title":"Label Embedding Online Hashing for Cross-Modal Retrieval","uri":"/lemon/"},{"categories":["Git"],"content":"问题描述 将项目文件push到GitHub上时,发现GitHub上的文件夹图标上有箭头,且无法打开。 ","date":"2022-11-06","objectID":"/git%E6%8A%A5%E9%94%99-github%E6%96%87%E4%BB%B6%E5%A4%B9%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E4%B8%94%E6%97%A0%E6%B3%95%E6%89%93%E5%BC%80/:1:0","tags":["Github","Git"],"title":"Git报错-GitHub文件夹出现箭头且无法打开","uri":"/git%E6%8A%A5%E9%94%99-github%E6%96%87%E4%BB%B6%E5%A4%B9%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E4%B8%94%E6%97%A0%E6%B3%95%E6%89%93%E5%BC%80/"},{"categories":["Git"],"content":"出错原因 当在自己的项目里clone了别人的项目,github就将他视为一个子系统模块,导致在上传代码时该文件夹上传失败,并在github上显示向右的白色箭头。 ","date":"2022-11-06","objectID":"/git%E6%8A%A5%E9%94%99-github%E6%96%87%E4%BB%B6%E5%A4%B9%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E4%B8%94%E6%97%A0%E6%B3%95%E6%89%93%E5%BC%80/:2:0","tags":["Github","Git"],"title":"Git报错-GitHub文件夹出现箭头且无法打开","uri":"/git%E6%8A%A5%E9%94%99-github%E6%96%87%E4%BB%B6%E5%A4%B9%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E4%B8%94%E6%97%A0%E6%B3%95%E6%89%93%E5%BC%80/"},{"categories":["Git"],"content":"解决方案 删除子文件夹里面的.git文件,执行如下命令: git rm --cached [文件夹名] git add [文件夹名] git commit -m \"commit messge\" git push origin main ","date":"2022-11-06","objectID":"/git%E6%8A%A5%E9%94%99-github%E6%96%87%E4%BB%B6%E5%A4%B9%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E4%B8%94%E6%97%A0%E6%B3%95%E6%89%93%E5%BC%80/:3:0","tags":["Github","Git"],"title":"Git报错-GitHub文件夹出现箭头且无法打开","uri":"/git%E6%8A%A5%E9%94%99-github%E6%96%87%E4%BB%B6%E5%A4%B9%E5%87%BA%E7%8E%B0%E7%AE%AD%E5%A4%B4%E4%B8%94%E6%97%A0%E6%B3%95%E6%89%93%E5%BC%80/"},{"categories":["Git"],"content":"问题描述 在将本地的远程仓库push到github上时,出现报错: ssh: Could not resolve hostname github.com: Temporary failure in name resolution fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. ","date":"2022-11-06","objectID":"/git%E6%8A%A5%E9%94%99-ssh%E7%9B%B8%E5%85%B3%E9%94%99%E8%AF%AF/:1:0","tags":["SSH","Github","Git","Ping"],"title":"Git报错-ssh相关错误","uri":"/git%E6%8A%A5%E9%94%99-ssh%E7%9B%B8%E5%85%B3%E9%94%99%E8%AF%AF/"},{"categories":["Git"],"content":"情况1 ssh错误 解决方案:重新设置ssh 1 重新在git设置身份的名字和邮箱 进入到需要提交的文件夹底下,执行命令: git config --global user.name \"yourname\" git config --global user.email \"your@email.com\" 注:yourname是你要设置的名字,your@email是你要设置的邮箱。 2 删除known_hosts文件 进入 .ssh 文件夹,手动删除 known_hosts 文件 3 重新设置ssh git输入命令: ssh-keygen -t rsa -C \"your@email.com\" 接着一路回车,系统会自动在 .ssh 文件夹下生成两个文件,id_rsa和id_rsa.pub,用记事本打开 id_rsa.pub,复制里面的全部内容。 4 在github上新建SSH key 进入GitHub网站的个人设置界面,在 SSH and GPG keys 中新建一个SSH key,将刚刚复制的密钥粘贴进去。 5 验证是否添加成功 在git中输入命令: ssh -T git@github.com 接着跳出一段话,输入命令:yes,提示重新设置成功! ","date":"2022-11-06","objectID":"/git%E6%8A%A5%E9%94%99-ssh%E7%9B%B8%E5%85%B3%E9%94%99%E8%AF%AF/:2:0","tags":["SSH","Github","Git","Ping"],"title":"Git报错-ssh相关错误","uri":"/git%E6%8A%A5%E9%94%99-ssh%E7%9B%B8%E5%85%B3%E9%94%99%E8%AF%AF/"},{"categories":["Git"],"content":"情况2 DNS错误 排除了ssh的问题后,在cmd中对目标地址进行ping操作: ping github.com 出现如下错误提示: Ping request could not find host github.com. Please check the name and try again. 说明DNS出现网络问题,解决方案如下: 1 首先获取 github.com IP 地址 IP 地址查询: Click 通过上述网站查询得到 github.com IP 地址如下 140.82.113.4 2 修改hosts文件 以管理员身份打开本地 C:\\Windows\\System32\\drivers\\etc 目录下的 hosts 文件,在文件最下方添加: 140.82.113.4 github.com 完成后保存即可。 3 再次ping github.com 此时再次ping github.com即可看到能够成功ping通。 $ ping 140.82.113.4 Pinging 140.82.113.4 with 32 bytes of data: Reply from 140.82.113.4: bytes=32 time=229ms TTL=42 Reply from 140.82.113.4: bytes=32 time=229ms TTL=42 Reply from 140.82.113.4: bytes=32 time=229ms TTL=42 Reply from 140.82.113.4: bytes=32 time=229ms TTL=42 Ping statistics for 140.82.113.4: Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), Approximate round trip times in milli-seconds: Minimum = 229ms, Maximum = 229ms, Average = 229ms 再次进行git push操作,可以顺利执行。 ","date":"2022-11-06","objectID":"/git%E6%8A%A5%E9%94%99-ssh%E7%9B%B8%E5%85%B3%E9%94%99%E8%AF%AF/:3:0","tags":["SSH","Github","Git","Ping"],"title":"Git报错-ssh相关错误","uri":"/git%E6%8A%A5%E9%94%99-ssh%E7%9B%B8%E5%85%B3%E9%94%99%E8%AF%AF/"},{"categories":["Android"],"content":"1 安装软件 打开命令行,进入apk文件所在目录 输入命令:adb install xxx.apk 2 踩雷记录 ","date":"2022-11-06","objectID":"/%E4%BD%BF%E7%94%A8adb%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85%E8%BD%AF%E4%BB%B6/:0:0","tags":["adb","Android"],"title":"使用adb命令安装软件","uri":"/%E4%BD%BF%E7%94%A8adb%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85%E8%BD%AF%E4%BB%B6/"},{"categories":["Android"],"content":"报错1 android adb devices offline 解决办法:重启adb服务 adb kill-server adb start-server ","date":"2022-11-06","objectID":"/%E4%BD%BF%E7%94%A8adb%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85%E8%BD%AF%E4%BB%B6/:0:1","tags":["adb","Android"],"title":"使用adb命令安装软件","uri":"/%E4%BD%BF%E7%94%A8adb%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85%E8%BD%AF%E4%BB%B6/"},{"categories":["Android"],"content":"报错2 Failed to install app-debug.apk: Failure [INSTALL_FAILED_TEST_ONLY: installPackageLI] 解决办法:允许安装test用的apk adb install -t app-debug.apk ","date":"2022-11-06","objectID":"/%E4%BD%BF%E7%94%A8adb%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85%E8%BD%AF%E4%BB%B6/:0:2","tags":["adb","Android"],"title":"使用adb命令安装软件","uri":"/%E4%BD%BF%E7%94%A8adb%E5%91%BD%E4%BB%A4%E5%AE%89%E8%A3%85%E8%BD%AF%E4%BB%B6/"},{"categories":["Mysql"],"content":"9 数据库应用开发(JAVA篇) ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:0:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"9.1 JDBC体系结构和简单的查询 JDBC的体系结构 JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。 Java 具有坚固、安全、易于使用、易于理解和可从网络上自动下载等特性,是编写数据库应用程序的杰出语言。所需要的只是 Java应用程序与各种不同数据库之间进行对话的方法。 JDBC可以在各种平台上使用Java,如Windows,Mac OS和各种版本的UNIX。 JDBC API支持用于数据库访问的两层和三层处理模型,但通常,JDBC体系结构由两层组成: JDBC API:这提供了应用程序到JDBC管理器连接。 JDBC驱动程序API:这支持JDBC管理器到驱动程序连接。 JDBC的核心组件 JDBC的核心组件包括: DriverManager: 此类管理数据库驱动程序列表。使用通信子协议将来自java应用程序的连接请求与适当的数据库驱动程序匹配。 Driver:此接口处理与数据库服务器的通信,我们很少会直接与Driver对象进行交互。而是使用DriverManager对象来管理这种类型的对象。 Connection:该界面具有用于联系数据库的所有方法。连接对象表示通信上下文,即,与数据库的所有通信仅通过连接对象。 Statement:使用从此接口创建的对象将SQL语句提交到数据库。除了执行存储过程之外,一些派生接口还接受参数。 ResultSet:在使用Statement对象执行SQL查询后,这些对象保存从数据库检索的数据。它作为一个迭代器,允许我们遍历其数据。 SQLException:此类处理数据库应用程序中发生的任何错误 使用步骤 构建JDBC应用程序涉及以下六个步骤: 导入包:需要包含包含数据库编程所需的JDBC类的包。大多数情况下,使用import java.sql.*就足够了。 注册JDBC驱动程序:要求您初始化驱动程序,以便您可以打开与数据库的通信通道。 打开连接:需要使用DriverManager.getConnection()方法创建一个Connection对象,该对象表示与数据库的物理连接。 执行查询:需要使用类型为Statement的对象来构建和提交SQL语句到数据库。 从结果集中提取数据:需要使用相应的ResultSet.getXXX()方法从结果集中检索数据。 释放资源:需要明确地关闭所有数据库资源,而不依赖于JVM的垃圾收集。 建立JDBC连接所涉及的编程可简单概括为以下四个步骤 导入JDBC包:将Java语言的import语句添加到Java代码中导入所需的类。 注册JDBC驱动程序:此步骤将使JVM将所需的驱动程序实现加载到内存中,以便它可以满足您的JDBC请求。 数据库URL配置:这是为了创建一个格式正确的地址,指向要连接到的数据库。 创建连接对象:最后,调用DriverManager对象的getConnection()方法来建立实际的数据库连接。 Class.forName(); 注册驱动程序最常见的方法是使用Java的Class.forName()方法,将驱动程序的类文件动态加载到内存中,并将其自动注册。 try { Class.forName(\"com.mysql.cj.jdbc.Driver\"); }catch(ClassNotFoundException ex) { System.out.println(\"Error: unable to load driver class!\"); System.exit(1); } 数据库URL配置 加载驱动程序后,可以使用DriverManager.getConnection()方法建立连接。为了方便参考,特列出三个重载的DriverManager.getConnection()方法: getConnection(String url) getConnection(String url,Properties prop) getConnection(String url,String user,String password) 创建数据库连接对象 String URL = \"jdbc:mysql://localhost:3306/dbname?serverTimezone=UTC\"; String USER = \"username\"; String PASS = \"password\" Connection conn = DriverManager.getConnection(URL, USER, PASS); 完整的连接地址: jdbc:mysql://127.0.0.1:3306/dbname?useUnicode=true\u0026characterEncoding=UTF8\u0026useSSL=false\u0026serverTimezone=UTC\" 关闭数据库连接 为确保连接关闭,您可以在代码中提供一个“finally”块。一个finally块总是执行,不管是否发生异常。 要关闭上面打开的连接,你应该调用close()方法如下: conn.close(); JDBC执行SQL语句 一旦获得了连接,我们可以与数据库进行交互。JDBC Statement和PreparedStatement接口定义了能够发送SQL命令并从数据库接收数据的方法和属性。 创建Statement对象 在使用Statement对象执行SQL语句之前,需要使用Connection对象的createStatement()方法创建Statement的一个实例,如下例所示: Statement stmt = null; try { stmt = conn.createStatement( ); . . . } catch (SQLException e) { . . . } finally { . . . } 执行Statement对象 创建Statement对象后,您可以使用它来执行一个SQL语句,其中有三个执行方法之一。 boolean execute(String SQL):如果可以检索到ResultSet对象,则返回一个布尔值true; 否则返回false。使用此方法执行SQL DDL语句或需要使用真正的动态SQL时。 int executeUpdate(String SQL):返回受SQL语句执行影响的行数。使用此方法执行预期会影响多个行的SQL语句,例如INSERT,UPDATE或DELETE语句。 ResultSet executeQuery(String SQL):返回一个ResultSet对象。当您希望获得结果集时,请使用此方法,就像使用SELECT语句一样。 关闭Statement对象 就像关闭一个Connection对象以保存数据库资源一样,由于同样的原因,还应该关闭Statement对象。 调用close()方法即可关闭Statement对象。如果先关闭Connection对象,它也会关闭Statement对象。但是,应始终显式关闭Statement对象,以确保正确清理。 Statement stmt = null; try { stmt = conn.createStatement( ); . . . } catch (SQLException e) { . . . } finally { stmt.close(); } PreparedStatement PreparedStatement的接口扩展了Statement接口,其优点是可以动态地提供参数。如果语句被多次执行,其执行效率比Statement高。 PreparedStatement pstmt = null; try { String SQL = \"Update Employees SET age = ? WHERE id = ?\"; pstmt = conn.prepareStatement(SQL); . . . } catch (SQLException e) { . . . } finally { pstmt.close(); } JDBC中的所有参数都用?符号代替,这被称为参数标记(又叫占位符)。在执行SQL语句之前,必须为每个参数提供值。 用setXXX(参数序号,参数值)方法将值绑定到对应参数,其中XXX代表要绑定到输入参数的值的Java数据类型。如果忘记提供值,将收到一个SQLException。参数的序号从1开始。 记得调用close()方法,显示关闭PreparedStatement对象。 ResultSet ResultSet对象维护指向结果集中当前行的游标。有多种类型的“游标”,如果没有指定任何ResultSet类型,则取缺省值TYPE_FORWARD_ONLY。 类型 类型描述 ResultSet.TYPE_SCROLL_INSENSITIVE 光标可以向前和向后滚动,结果集对创建结果集后发生的数据库的其他更改不敏感。 ResultSet.TYPE_SCROLL_SENSITIVE 光标可以向前和向后滚动,结果集对创建结果集之后发生的其他数据库所做的更改敏感。 ResultSet.TYPE_FORWARD_ONLY 光标只能在结果集中向前移动。 ResultSet的遍历 用ResultSet的next()方法取得游标当前行的值,并用getXXX(列名)方","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:1:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"9.2 条件不确定的查询 JDBC的Statement类方法executeQuery(sql)可以执行一条确定的sql语句,如果要执行的sql语句带有变化的部分,比如每个客户的输入的用户名与密码都会不同. 这种情况,有两种解决方案: 把变量直接拼接到sql语句中 下例中,假定变量userName(类型String)在此前已被赋值: statement = connection.createStatement(); String sql = \"select * from user where username = '\" + userName + \"';\"; resultSet = statement.executeQuery(sql); 用PreparedStatement 用PreparedStatement类,把sql语句中变化的部分当成参数: String sql = \"select * from user where username = ? ;\"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1,userName); resultSet = preparedStatement.executeQuery(); ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:2:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"9.3 JDBC的插入操作 在成功连接数据库后,实例化Statement或PreparedStatement类的一个对象。该对象关联一条sql insert语句,然后调用对象的executeUpdate()方法即可。以PreparedStatement为例: String sql = \"insert into user values(?,?)\"; pps = connection.prepareStatement(sql); pps.setString(1,loginName); pps.setString(2,loginPass); int n = pps.executeUpdate(); if (n \u003e 0) { System.out.println(\"执行成功,影响行数:\" + n); } else { System.out.println(\"执行失败.\"); } 第2行出现的pps和connection分别为PreparedStatment和Connection类的一个对象(均在该代码段之前已实例化)。第3行和第4行出现的loginName和loginPass是两个String对象,假定此前已被赋值。 ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:3:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"9.4 事务处理 JDBC的事务处理 JDBC缺省情形下,Statement和PreapredStatement均自动为一个事务。因此,当一个Statement调了executeUpdate()方法后,所执行的SQL语句自动提交,其对数据库的修改不可再撤销。 但调用Connection.setAutoCommit(boolean autoCommit)方法可以改变缺省设置: connection.setAutoCommit(false); 将使本会话期内的语句不再自动提交,必须调用Connection的以下方法手动提交: commit() rollback() 前者为正常提交,后者表示事务回滚,撤销所有修改。 JDBC的事务隔离级别 JDBC通过调用Connetion的以下方法设置事务的隔离级别: void setTransactionIsolation(int level) 如果没有调用该方法设置隔离级别,将采用DBMS缺省的隔离级别,对MySQL而言,即repeatable read。JDBC支持以下隔离级别: static int TRANSACTION_NONE static int TRANSACTION_READ_UNCOMMITTED static int TRANSACTION_READ_COMMITTED static int TRANSACTION_REPEATABLE_READ static int TRANSACTION_SERIALIZABLE 它们对应的int值依次为0,1,2,4和8。 不是每个DBMS都支持以上所有的隔离级别。MySQL支持后面4个隔离级别,且缺省状态下为TRANSACTION_REPEATABLE_READ。Oracle支持其中的两个,缺省值为TRANSACTION_READ_COMMITTED。 10 数据库设计与实现 ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:4:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"10.1 从需求分析到逻辑模型 ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:5:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"业务功能描述 设计一个影院管理系统。影院对当前的放映厅和电影进行排片,顾客到来后,可以购买任一排场的电影票,进入对应放映厅观看。系统中有以下实体集: 电影(movie):属性有标识号(movie_ID)、电影名(title)、类型(type)、时长(runtime)、首映日期(release_date)、导演姓名(director)、主演姓名(starring)。 顾客(customer):属性有标识号(c_ID)、姓名(name)、手机号(phone)。 放映厅(hall):属性有标识号(hall_ID)、放映模式(mode)、容纳人数(capacity)、位置(location)。 排场(schedule):属性有标识号(schedule_ID)、日期(date)、时间(time)、票价(price)、票数(number)。 电影票(ticket):属性有标识号(ticket_ID)、座位号(seat_num)。 实体间的关系描述如下: ①. 顾客和电影票有一对多的购买关系。每位顾客可以买多张电影票,每张电影票被一位顾客购买。 ②. 电影票和排场有多对一的属于关系。一张电影票只属于一个排场,一个排场有多张电影票。 ③. 排场和电影有一对多的放映关系。每个排场放一部电影,每部电影可以在多个排场放映。 ④. 排场和放映厅有一对多的位于关系。每个排场位于一个放映厅,每个放映厅可以安排多个排场。 ER图: 关系模式: movie(movie_ID,title,type,runtime,release_date,director,starring),主码:(movie_ID) customer(c_ID,name,phone),主码:(c_ID) hall(hall_ID,mode,capacity,location),主码:(hall_ID) schedule(schedule_ID,date,time,price,number,hall_ID,movie_ID),主码:(schedule_ID);外码:(hall_ID,movie_ID) ticket(ticket_ID,seat_num,c_ID,schedule_ID),主码:(ticket_ID);外码:(c_ID,schedule_ID) ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:5:1","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Mysql"],"content":"10.2 建模工具简介 1. ERWIN erwin Data Modeler行业领先的数据建模解决方案,具有直观的设计和存档功能,支持管理整个企业内任何存储位置的任何数据。但收费较贵,不过,大学学生可以申请erwin Data Modeler Academic Edition,有效期1年。可以去官网下载文档了解具体功能和使用方法: 中文官网:http://www.erwinchina.com/ 2.Navicat Navicat支持几乎所有你常用的DBMS,支持概念模型,逻辑模型和物理模型,可根据模型文件生成任何DBMS的脚本。支持Forward Engineering和Reverse Engineering。 该软件也是收费。 Navicat同时也是用户较多的DBMS客户端管理工具。 中文官网: https://www.navicat.com.cn/ **3.Microsoft Visio ** 现在,Visio已从Office分离出来,需要单独购买。支持陈氏E-R图和Crow’s foot。可用来表达概念模型,逻辑模型等。 4.Draw.io 这是最容易获得的建模工具,只需要web browser的地址栏里输入URL即可调出: draw.io 或者: https://app.diagrams.net/ 输入前者会自动跳转到后者。 比较适合建立概念模型和逻辑模型。既支持陈氏ER图,也支持Crow’s footsER图。但它不能跟具体的DBMS连接,不支持正向及逆向工程。 5. MySQL Workbench MySQL社区版自带的免费工具。也是比较好用的图形界面客户端管理工具。支持正向及逆向工具。 官网可下载使用手册。 ","date":"2022-10-28","objectID":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/:6:0","tags":["Mysql"],"title":"Mysql数据库基本语法(五)","uri":"/mysql%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E4%BA%94/"},{"categories":["Git"],"content":"基本流程 # 初始化仓库 git init # 将本地库关联至远程仓库 git remote add origin git@github.com:....github.io.git # 查看当前修改状态 git status # 添加修改过得文件, . 表示所有,也可以指定文件 git add . # \"\"里面的内容就是提交内容的说明信息 git commit -m \"first commit\" # 第一次提交方法1 git push -u -f origin main #第一次提交方法2 git pull origin main --allow-unrelated-histories git push -u origin main # 以后提交 git push ","date":"2022-10-21","objectID":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/:1:0","tags":["Git","Github","Gitee"],"title":"Git基本用法\u0026报错记录","uri":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/"},{"categories":["Git"],"content":"其他用法 1 修改分支名 git branch -m oldBranchName newBranchName 2 取消与远程仓库的关联 git remote remove origin 3 实现本地库同时关联GitHub和Gitee # 初始化仓库 git init # 将本地库同时和GitHub、Gitee的远程仓库关联 git remote add github git@github.com:bertilchan/gitTest.git git remote add gitee git@gitee.com:bertil/git-test.git # 查看关联的远程库信息 git remote -v # 添加修改,和原来一样 git add . git commit -m \"first commit\" # 分别提交 git push -u github main git push -u gitee main ","date":"2022-10-21","objectID":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/:2:0","tags":["Git","Github","Gitee"],"title":"Git基本用法\u0026报错记录","uri":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/"},{"categories":["Git"],"content":"报错记录 ","date":"2022-10-21","objectID":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/:3:0","tags":["Git","Github","Gitee"],"title":"Git基本用法\u0026报错记录","uri":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/"},{"categories":["Git"],"content":"报错1 ssh: Could not resolve hostname github.com: Temporary failure in name resolution fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. 解决方案见:Git报错-ssh相关错误 ","date":"2022-10-21","objectID":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/:3:1","tags":["Git","Github","Gitee"],"title":"Git基本用法\u0026报错记录","uri":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/"},{"categories":["Git"],"content":"报错2 push后GitHub文件夹出现箭头且无法打开 解决方案见:Git报错-GitHub文件夹出现箭头且无法打开 ","date":"2022-10-21","objectID":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/:3:2","tags":["Git","Github","Gitee"],"title":"Git基本用法\u0026报错记录","uri":"/git%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95%E6%8A%A5%E9%94%99%E8%AE%B0%E5%BD%95/"},{"categories":["Go"],"content":"1 路由与控制器 ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:1:0","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"1 路由规则 一条路由规则由:http请求方法 , url路径 , 控制器函数 组成 1.http请求方法 GET POST PUT DELETE 2.url路径 静态url路径 带路径参数的url路径 带星号(*)模糊匹配参数的url路径 // 例子1, 静态Url路径, 即不带任何参数的url路径 /users/center /user/101 /food/100 // 例子2,带路径参数的url路径,url路径上面带有参数,参数由冒号(:)跟着一个字符串定义。 // 路径参数值可以是数值,也可以是字符串 //定义参数:id, 可以匹配/user/1, /user/899 /user/xiaoli 这类Url路径 /user/:id //定义参数:id, 可以匹配/food/2, /food/100 /food/apple 这类Url路径 /food/:id //定义参数:type和:page, 可以匹配/foods/2/1, /food/100/25 /food/apple/30 这类Url路径 /foods/:type/:page // 例子3. 带星号(*)模糊匹配参数的url路径 // 星号代表匹配任意路径的意思 //匹配:/foods/1, /foods/200, /foods/1/20, /foods/apple/1 //以/foods/ 开头的所有路径都匹配 /foods/* 3.url路径匹配顺序 如果出现,一个http请求路径匹配多个定义的url路径,echo框架按下面顺序匹配,先匹配到那个就用那个定义。 匹配静态url路径 匹配带路径参数的url路径 匹配带星号(*)模糊匹配参数的url路径 4.控制器函数 控制器函数接受一个上下文参数,并返回一个错误。可以通过上下文参数,获取http请求参数,响应http请求。 func HandlerFunc(c echo.Context) error{} ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:1:1","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"2 示例 实际项目开发中不要把路由定义和控制器函数都写在一个go文件,不方便维护。 //实例化echo对象。 e := echo.New() //定义post请求, url路径为:/users, 绑定saveUser控制器函数 e.POST(\"/users\", saveUser) //定义get请求,url路径为:/users/:id (:id是参数,例如: /users/10, 会匹配这个url模式),绑定getUser控制器函数 e.GET(\"/users/:id\", getUser) //定义put请求 e.PUT(\"/users/:id\", updateUser) //定义delete请求 e.DELETE(\"/users/:id\", deleteUser) //控制器函数实现 func saveUser(c echo.Context) error { ...忽略实现... } func getUser(c echo.Context) error { ...忽略实现... } func updateUser(c echo.Context) error { ...忽略实现... } func deleteUser(c echo.Context) error { ...忽略实现... } ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:1:2","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"2 处理请求参数 ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:2:0","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"1 绑定数据 通过将请求参数绑定到一个struct对象的方式获取数据。这种方式获取请求参数支持json、xml、k/v键值对等多种方式。 // User 结构体定义 type User struct { Name string `json:\"name\" form:\"name\" query:\"name\"` Email string `json:\"email\" form:\"email\" query:\"email\"` } 控制器代码: // Handler func(c echo.Context) (err error) { u := new(User) //调用echo.Context的Bind函数将请求参数和User对象进行绑定。 if err = c.Bind(u); err != nil { return } //请求参数绑定成功后 u 对象就保存了请求参数。 //这里直接将请求参数以json格式显示 //注意:User结构体,字段标签定义中,json定义的字段名,就是User对象转换成json格式对应的字段名。 return c.JSON(http.StatusOK, u) } ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:2:1","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"2 获取post请求数据 通过echo.Context对象的 FormValue 函数可以直接获取post请求参数。 通过FormValue函数获取参数的值,数据类型都是String类型, 如果需要其他类型的数据,需要自己转换数据格式。 // Handler func(c echo.Context) error { //获取name参数 name := c.FormValue(\"name\") //直接输出name参数 return c.String(http.StatusOK, name) } ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:2:2","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"3 获取get请求数据 通过echo.Context对象的 QueryParam 函数可以直接获取get请求参数。 // Handler func(c echo.Context) error { //获取name参数, 通过QueryParam获取的参数值也是String类型。 name := c.QueryParam(\"name\") //直接输出name参数 return c.String(http.StatusOK, name) }) ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:2:3","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"4 获取path路径参数 通过echo.Context对象的 Param 获取,url路径参数。 //例子: url路由规则为/users/:name , :name为参数。 e.GET(\"/users/:name\", func(c echo.Context) error { //获取路径参数:name的值 name := c.Param(\"name\") //如果请求url为: /users/tizi365 则name的值为tizi365 //Param获取的值也是String类型 return c.String(http.StatusOK, name) }) ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:2:4","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"3 处理请求结果 1 以字符串方式响应 String(code int, s string) error 2 以json格式响应 JSON(code int, i interface{}) error 3 以xml格式响应 XML(code int, i interface{}) error 4 以文件格式响应 5 设置http响应头 func(c echo.Context) error { //设置http响应 header c.Response().Header().Add(\"tizi\", \"tizi365\") return c.String(200, \"欢迎访问tizi360.com!\") } ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:3:0","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"4 访问静态资源文件 echo通过static中间件支持静态资源文件的访问。我们可以通过echo.Static函数初始化static中间件。 Static(prefix, root string) *Route //初始化echo实例 e := echo.New() //设置Static中间件 //如果我们访问 /res/tizi.jpg这个url路径,实际上就是访问static/tizi.jpg这个路径的内容 e.Static(\"/res\", \"static\") 我们也可以通过Echo.File函数为一个url地址绑定一个静态资源文件。 //初始化echo实例 e := echo.New() //访问 / 就是访问public/index.html文件, index.html相当于站点默认首页 e.File(\"/\", \"public/index.html\") //访问/favicon.ico 就是访问images/favicon.ico文件, 相当于为站点设置了图标 e.File(\"/favicon.ico\", \"images/favicon.ico\") ","date":"2022-10-20","objectID":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/:4:0","tags":["Echo","Go"],"title":"Echo框架入门","uri":"/echo%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8/"},{"categories":["Go"],"content":"在go语言标准库中,net包提供了可移植的网络I/O接口,包括TCP/IP、UDP、域名解析和Unix域socket。 ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:0:0","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"1 服务端 ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:1:0","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"1. 解析地址 在TCP服务端我们需要监听一个TCP地址,因此建立服务端前我们需要生成一个正确的TCP地址,这就需要用到 Resolve 函数。 // ResolveTCPAddr函数会输出一个TCP连接地址和一个错误信息 func ResolveTCPAddr(network, address string) (*TCPAddr, error) // 解析IP地址 func ResolveIPAddr(net, addr string) (*IPAddr, error) // 解析UDP地址 func ResolveUDPAddr(net, addr string) (*UDPAddr, error) // 解析Unix地址 func ResolveUnixAddr(net, addr string) (*UnixAddr, error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:1:1","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"2. 监听请求 我们可以通过 Listen 方法监听我们解析后的网络地址。 // 监听net类型,地址为laddr的地址 func Listen(net, laddr string) (Listener, error) // 监听TCP地址 func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error) // 监听IP地址 func ListenIP(netProto string, laddr *IPAddr) (*IPConn, error) // 监听UDP地址 func ListenMulticastUDP(net string, ifi *Interface, gaddr *UDPAddr) (*UDPConn, error) func ListenUDP(net string, laddr *UDPAddr) (*UDPConn, error) // 监听Unix地址 func ListenUnixgram(net string, laddr *UnixAddr) (*UnixConn, error) func ListenUnix(net string, laddr *UnixAddr) (*UnixListener, error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:1:2","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"3. 接受请求 TCPAddr 实现了两个接受请求的 Accept 方法,两者代码实现其实是一样的,唯一的区别是第一种返回了一个对象,第二种返回了一个接口。 func (l *TCPListener) AcceptTCP() (*TCPConn, error) func (l *TCPListener) Accept() (Conn, error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:1:3","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"4. 连接配置 // 配置监听器超时时间:超过t之后监听器自动关闭,0表示不设置超时时间 func (l *TCPListener) SetDeadline(t time.Time) error // 关闭监听器 func (l *TCPListener) Close() error ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:1:4","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"5. 编写一个服务器 func main() { // 解析服务端监听地址,本例以tcp为例 addr, err := net.ResolveTCPAddr(\"tcp\", \"127.0.0.1:8000\") if err != nil { log.Panic(err) } // 创建监听器 listen, err := net.ListenTCP(\"tcp\", addr) if err != nil { log.Panic(err) } for { // 监听客户端连接请求 conn, err := listen.AcceptTCP() if err != nil { continue } // 处理客户端请求 这个函数可以自己编写 go HandleConnectionForServer(conn) } } ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:1:5","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"2 客户端 ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:2:0","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"1. 解析地址 在TCP服务端我们需要监听一个TCP地址,因此建立服务端前我们需要生成一个正确的TCP地址,这就需要用到 Resolve 函数了。 // ResolveTCPAddr函数会输出一个TCP连接地址和一个错误信息 func ResolveTCPAddr(network, address string) (*TCPAddr, error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:2:1","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"2. 发送连接请求 // DialIP的作用类似于IP网络的拨号 func DialIP(network string, laddr, raddr *IPAddr) (*IPConn, error) // Dial 连接到指定网络上的地址,涵盖 func Dial(network, address string) (Conn, error) // 这个方法只是在Dial上面设置了超时时间 func DialTimeout(network, address string, timeout time.Duration) (Conn, error) // DialTCP 专门用来进行TCP通信的 func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error) // DialUDP 专门用来进行UDP通信的 func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error) // DialUnix 专门用来进行 Unix 通信 func DialUnix(network string, laddr, raddr *UnixAddr) (*UnixConn, error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:2:2","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"3. 编写一个客户端 func main() { // 解析服务端地址 RemoteAddr, err := net.ResolveTCPAddr(\"tcp\", \"127.0.0.1:8000\") if err != nil { panic(err) } // 解析本地连接地址 LocalAddr, err := net.ResolveTCPAddr(\"tcp\", \"127.0.0.1\") if err != nil { panic(err) } // 连接服务端 conn, err := net.DialTCP(\"tcp\", LocalAddr, RemoteAddr) if err != nil { panic(err) } // 连接管理 HandleConnectionForClient(conn) } ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:2:3","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"3 域名解析 ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:3:0","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"1. DNS正向解析 CNAME 被称为规范名字。这种记录允许您将多个名字映射到同一台计算机。 通常用于同时提供WWW和MAIL服务的计算机。 //域名解析到cname func LookupCNAME(name string) (cname string, err error) //域名解析到地址 func LookupHost(host string) (addrs []string, err error) //域名解析到地址[]IP结构体.可以对具体ip进行相关操作(是否回环地址,子网,网络号等) func LookupIP(host string) (addrs []IP, err error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:3:1","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"2. DNS反向解析 // 根据ip地址查找主机名地址(必须得是可以解析到的域名)[dig -x ipaddress] func LookupAddr(addr string) (name []string, err error) ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:3:2","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"4 其他常用接口 ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:4:0","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"1. Conn接口 type Conn interface { // Read从连接中读取数据 // Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真 Read(b []byte) (n int, err error) // Write从连接中写入数据 // Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真 Write(b []byte) (n int, err error) // Close方法关闭该连接 // 并会导致任何阻塞中的Read或Write方法不再阻塞并返回错误 Close() error // 返回本地网络地址 LocalAddr() Addr // 返回远端网络地址 RemoteAddr() Addr // 设定该连接的读写deadline,等价于同时调用SetReadDeadline和SetWriteDeadline // deadline是一个绝对时间,超过该时间后I/O操作就会直接因超时失败返回而不会阻塞 // deadline对之后的所有I/O操作都起效,而不仅仅是下一次的读或写操作 // 参数t为零值表示不设置期限 SetDeadline(t time.Time) error // 设定该连接的读操作deadline,参数t为零值表示不设置期限 SetReadDeadline(t time.Time) error // 设定该连接的写操作deadline,参数t为零值表示不设置期限 // 即使写入超时,返回值n也可能\u003e0,说明成功写入了部分数据 SetWriteDeadline(t time.Time) error } ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:4:1","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"2. PacketConn接口 type PacketConn interface { // ReadFrom方法从连接读取一个数据包,并将有效信息写入b // ReadFrom方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真 // 返回写入的字节数和该数据包的来源地址 ReadFrom(b []byte) (n int, addr Addr, err error) // WriteTo方法将有效数据b写入一个数据包发送给addr // WriteTo方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真 // 在面向数据包的连接中,写入超时非常罕见 WriteTo(b []byte, addr Addr) (n int, err error) // Close方法关闭该连接 // 会导致任何阻塞中的ReadFrom或WriteTo方法不再阻塞并返回错误 Close() error // 返回本地网络地址 LocalAddr() Addr // 设定该连接的读写deadline SetDeadline(t time.Time) error // 设定该连接的读操作deadline,参数t为零值表示不设置期限 // 如果时间到达deadline,读操作就会直接因超时失败返回而不会阻塞 SetReadDeadline(t time.Time) error // 设定该连接的写操作deadline,参数t为零值表示不设置期限 // 如果时间到达deadline,写操作就会直接因超时失败返回而不会阻塞 // 即使写入超时,返回值n也可能\u003e0,说明成功写入了部分数据 SetWriteDeadline(t time.Time) error } ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:4:2","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"3. Error接口 package net type Error interface{ Timeout() bool // 错误是否超时 Temporary() bool // 是否是临时错误 } ","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:4:3","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["Go"],"content":"5 示例 server.go /*服务器代码*/ package main import ( \"fmt\" \"net\" ) func main() { //创建listener listener, err := net.Listen(\"tcp\", \"localhost:50000\") //用来监听和接收来自客户端的请求 if err != nil { fmt.Println(\"Error Listening\", err.Error()) //Error是个什么? return //终止程序 } //无限循环,监听并接受来自客户端的连接 for { conn, err := listener.Accept() if err != nil { fmt.Println(\"Error accepting\", err.Error()) return //终止程序 } go doServerStuff(conn) //? } } func doServerStuff(conn net.Conn) { for { buf := make([]byte, 512) len, err := conn.Read(buf) //获取客户端发送字节数 if err != nil { fmt.Println(\"Error reading\", err.Error()) return //终止程序 } fmt.Printf(\"Received data:%v\\n\", string(buf[:len])) } } client.go package main import ( \"bufio\" \"fmt\" \"net\" \"os\" \"strings\" ) func main() { //创建和服务器的连接 conn, err := net.Dial(\"tcp\", \"localhost:50000\") if err != nil { fmt.Println(\"Error dialing\", err.Error()) //由于目标计算机积极拒绝而无法创建连接 return //终止程序 } inputReader := bufio.NewReader(os.Stdin) //接收来自键盘的输入 fmt.Println(\"First,what is your name?\") clientName, _ := inputReader.ReadString('\\n') trimmedClient := strings.Trim(clientName, \"\\r\\n\") // Windows 平台下用 \"\\r\\n\",Linux平台下使用 \"\\n\" //给服务器发送信息知道程序退出 for { fmt.Println(\"What to send to the server? Type Q to quit.\") input, _ := inputReader.ReadString('\\n') trimmedInput := strings.Trim(input, \"\\r\\n\") if trimmedInput==\"Q\"{ return } _,err=conn.Write([]byte(trimmedClient+\" says: \"+trimmedInput)) } } socket.go package main import( \"fmt\" \"io\" \"net\" ) func main(){ var( host=\"www.apache.org\" port=\"80\" remote=host+\":\"+port msg string=\"GET / \\n\" data=make([]uint8,4096) read=true count=0 ) //创建一个socket conn,err:=net.Dial(\"tcp\",remote) //发送我们的消息:一个http GET请求 io.WriteString(conn,msg) //读取服务器的响应 for read{ count,err=conn.Read(data) read=(err==nil) fmt.Printf(string(data[0:count])) } conn.Close() } dial.go //make a connection with www.example.org: package main import( \"fmt\" \"net\" \"os\" ) func main(){ conn,err:=net.Dial(\"tcp\",\"192.0.32.10:80\") //tcp ipv4 checkConnection(conn,err) conn,err=net.Dial(\"udp\",\"192.0.32.10:80\") //udp checkConnection(conn,err) conn,err=net.Dial(\"tcp\",\"[2620:0:2d0:200::10]:80\") //tcp ipv6 checkConnection(conn,err) } func checkConnection(conn net.Conn,err error){ if err!=nil{ fmt.Printf(\"error %v connecting!\",err) os.Exit(1) } fmt.Printf(\"Connection is made with %v\\n\",conn) } server_simple.go package main import( \"flag\" \"fmt\" \"net\" \"syscall\" ) const maxread=25 func main(){ flag.Parse() //服务器地址和端口通过命令行传入参数,并通过flag包来读取这些参数 if flag.NArg() != 2 { //检查是否按照期望传入了2个参数 panic(\"usage: host port\") //此函数停止执行,并将控制权返还给其调用者 } hostAndPort := fmt.Sprintf(\"%s:%s\", flag.Arg(0), flag.Arg(1)) //格式化成字符串 listener := initServer(hostAndPort) for { conn, err := listener.Accept() //接受请求,返回conn对象 checkError(err, \"Accept: \") go connectionHandler(conn) } } func initServer(hostAndPort string)net.Listener{ serverAddr,err:=net.ResolveTCPAddr(\"tcp\",hostAndPort) //解析TCP地址 checkError(err,\"Resolving address:port failed: '\"+hostAndPort+\"'\") listener,err:=net.ListenTCP(\"tcp\",serverAddr) //监听请求 checkError(err,\"ListenTCP: \") println(\"Listening to: \",listener.Addr().String()) return listener } func connectionHandler(conn net.Conn){ connFrom:=conn.RemoteAddr().String() //获取客户端的地址 println(\"Connection from: \",connFrom) sayHello(conn) for{ var ibuf []byte=make([]byte, maxread+1) //设置maxread防止溢出 length,err:=conn.Read(ibuf[0:maxread]) //读取连接中的内容 ibuf[maxread]=0 switch err{ case nil: handleMsg(length,err,ibuf) case syscall.EAGAIN: continue //重新尝试连接 default: goto DISCONNECT } } DISCONNECT: err:=conn.Close() //关闭连接 println(\"Closed connection: \",connFrom) checkError(err,\"Close: \") } func sayHello(to net.Conn) { obuf := []byte{'L', 'e', 't', '\\'', 's', ' ', 'G', 'O', '!', '\\n'} wrote, err := to.Write(obuf) //发送message给客户端 checkError(err, \"Write: wrote \"+string(wrote)+\" bytes.\") } func handleMsg(length int, err error, msg []byte) { if length \u003e 0 { print(\"\u003c\", length, \":\") for i := 0; ; i++ { if msg[i] == 0 { break } fmt.Printf(\"%c\", msg[i]) } print(\"\u003e\") } } func checkError(error error, info string) { if error !=","date":"2022-10-20","objectID":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/:5:0","tags":["Go"],"title":"Go-net标准库应用","uri":"/go-net%E6%A0%87%E5%87%86%E5%BA%93%E5%BA%94%E7%94%A8/"},{"categories":["深度学习"],"content":"本文为论文 Vision GNN: An Image is Worth Graph of Nodes 的阅读笔记。 论文下载:https://arxiv.org/abs/2206.00272 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:0:0","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"引言 网络架构在基于深度学习的计算机视觉中起着关键作用。广泛使用的CNN和 transformer(变换器)将图像视为 grid(网格)或 sequence(序列)结构,这对于捕捉不规则、复杂的物体来说是不灵活的。本文建议将图像表示为一个 graph 结构,并引入一个新的 Vision GNN(ViG)架构来提取视觉任务的图层特征。 文章主要工作: 介绍了计算机视觉方面的现有模型方法和成果 介绍ViG模型的构建过程及工作原理,为未来的研究提供有用的灵感和经验 通过图像分类和物体检测实验证明了ViG模型在视觉任务中的有效性 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:1:0","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"1 相关研究 CNN 曾经是计算机视觉中标准的网络结构,但近来 transformer with attention mechanism 、MLP-based 等模型也在不断发展,这些正在将视觉模型推向一个前所未有的高度。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:2:0","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"1.1 3种图像结构 不同的网络结构以不同的方式处理输入的图像,通常有grid, sequence ,graph 3种,如下图所示。在 grid 和 sequence 结构中,节点只按空间位置排序;在 graph 结构中,节点是通过其内容连接的,不受局部位置的限制。 CNN 在图像上应用滑动窗口,并引入移位变异性和位置性;最近的 vision transformer 或 MLP 将图像视为 a sequence of patches(补丁序列)。 由于物体形状通常不是规则的四边形,常用的 grid 或 sequence 结构处理起图像来不够灵活,所以在本文中采用 graph 结构。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:2:1","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"1.2 3种模型 CNN:曾经是计算机视觉中的主流网络结构,已经成功地应用于各种视觉任务,如图像分类、物体检测和语义分割。CNN模型在过去的十年里发展迅速,代表性的模型包括ResNet、MobileNet和NAS。 Vision transformer:从2020年开始,被引入到视觉任务中,ViT的一些变体开始被提出来以提高视觉任务的性能。主要的改进包括金字塔结,局部注意和位置编码。 MLP:通过专门设计的模块,MLP可以达到有竞争力的性能,并且在一般的视觉任务(如物体检测和分割)上工作。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:2:2","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"1.3 GNN模型 1. GNN\u0026GCN GNN:图神经网络,由于传统的CNN网络无法表示顶点和边这种关系型数据,便出现了图神经网络解决这种图数据的表示问题,这属于CNN往图方向的应用扩展 GCN:图卷积神经网络,GNN在训练过程中,有将attention引入图结构的,有将门控机制引入图结构的,还有将卷积引入图结构的,引入卷积的GNN就是GCN,通过提取空间特征来进行学习 2. 发展 Micheli提出了早期的提出了早期的基于空间的GCN,Bruna等人提出了基于频谱的GCN,近几年来基于这两种GCN的改进和扩展也被提出。 3. 应用 GCN通常被应用于图数据,如社会网络、引文网络和生化图;在计算机视觉领域的应用主要包括点云分类、场景图生成和动作识别。 GCN只能解决自然形成的图的特定视觉任务,对于计算机视觉的一般应用,我们需要一个基于GCN的骨干网络来直接处理图像数据。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:2:3","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"2 ViG模型 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:3:0","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"2.1 模型构建 Image→Graph 首先基于 graph 结构建立视觉图形神经网络,用于视觉任务。将输入的图像划分为若干个 patches(补丁),并将每个斑块视为一个 node (节点)。1 . 对于一个大小为 $H×W×3$ 的图像,我们将其分为 N 个补丁,把每个补丁转化为一个特征向量 $x_i∈R^D$,得到 $X = [x_1,x_2,…,x_N ]$, 其中 $D$ 是特征维度。将特征看做无序节点$V={v_1,v_2,…,v_N}$,节点$v_i$的k邻近节点记为$N(v_i)$,对每个$v_j∈N(v_i)$添加$v_j$到$v_i$的边$e_ji$。 最终得到图$G = (V,E) $,我们把图的构建过程记为$G = G(X)$。 图层处理 图卷积层可以通过聚合其邻居节点的特征在节点之间交换信息。具体操作方式为: $G' = F(G, W)=Update(Aggregate(G, W_agg), W_update) $ 其中,$W_agg$和 $W_update$是聚合、更新操作的可学习权重。 聚合:通过聚合邻居节点的特征来计算一个节点的表征 更新:进一步合并聚合的特征 通过最大相对卷积处理图层面,记为$X' = GraphConv(X)$。 $x_i' = h(x_i, g(x_i, N(x_i), W_agg), W_update)$ $g(·) = x_i'' = [x_i, max(${$x_j - x_i|j∈N(x_i)$}] $h(·) = x_i' = x_i'‘W_update$ . 接着进行图卷积的多头更新操作(有利于特征多样性),将聚合后的特征 $x_i’'$ 分割成 $h$ 个头,然后分别以不同的权重更新这些头,得到: $x_i' = [head^1W^1update, head^2W^2update,…head^hW^hupdate]$ ViG block ViG的2个基本模块 Graph模块:是在图卷积的基础上构建的,用于聚合和更新图形信息,可以缓解传统GNN的过度平滑现象 FFN模块:带有两个线性层,用于节点特征转换和鼓励节点多样性 以前的GCN通常重复使用卷积层来提取图形数据的聚合特征,这会导致过度平滑的现象 ,降低节点特征的显著性,如下图所示所示。为了解决这个问题,本文在ViG块中引入了更多的特征转换和非线性激活。 我们在图卷积前后应用一个线性层,将节点特征投射到同一领域,增加特征多样性。在图卷积之后插入一个非线性激活函数以避免层崩溃。我们称升级后的模块为Grapher模块,给定输入特征$X∈R^N×^D$ ,则可得到:$Y = σ(GraphConv(XW_in))W_out + X$ 2 . 其中$W_in$和$W_out$是全连接层的权重,σ是激活函数。为了进一步提高特征转换能力,我们在每个节点上利用前馈网络(FFN):$Z = σ(YW_1)W_2 + Y$ 其中$W_1$和$W_2$是全连接层的权重。Graph模块和FFN模块的堆叠构成了ViG块,作为构建网络的基本单元。基于图像的graph结构和ViG块,我们可以建立ViG网络。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:3:1","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"2.2 网络框架 各项同性结构:指主体在整个网络中具有同等大小和形状的特征 金字塔结构:考虑了图像的多尺度特性,提取特征的空间大小逐渐变小 在计算机视觉领域,常用的结构有各向同性结构和金字塔结构。为了与其他类型的神经网络有更普遍的比较,文章分别为ViG建立了这两种网络结构。 各向同性结构 文章建立了3个大小不同的各向同性ViG架构。为了扩大感受野,邻居结点的数量K从9线性增加到18;头的数量被设定为 h = 4。详情如下表:3 金字塔结构 文章建立了4个大小不同的金字塔ViG模型。详情如下:4 位置编码 为了表示节点的位置信息,文章为每个节点特征添加一个位置向量:$x_i←x_i+e_i$ ;金字塔结构中可以进一步使用相对位置编码。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:3:2","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"2.3 模型优点 graph 是广义的数据结构,grid 和 sequence 可以看做 graph 的特例 graph 更灵活,可以对复杂、不规则的物体进行建模 一个物体可以被看作是由各个部分组成的,graph 结构可以构建他们的联系 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:3:3","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"3 实验 top1 accuracy:预测的label取最后概率向量里面最大的那一个作为预测结果,如果预测结果中概率最大的分类正确,则预测正确,否则预测错误。 top5 accuracy:最后概率向量最大的前五名中,只要出现了正确概率即为预测正确,否则预测错误。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:4:0","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"3.1 实验结果 本文分别将各向同性结构、金字塔结构的ViG与同样结构的CNN、转化器和 MLPs对比,可以看出: 将图片视作Graph能够在计算机视觉任务中取得非常好的结果 和各向同性结构相比,金字塔结构的ViG具有更好的性能 各向同性结构的实验结果: 金字塔结构的实验结果: ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:4:1","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"3.2 消融研究 消融研究:通过删除部分网络并研究网络的性能来更好的了解网络。 文章以各向同性的ViG-Ti为基础架构,在ImageNet分类任务上进行了消融研究,结果如下: 通过改变图卷积的类型,发现不同图卷积的Top-1准确率很高,说明ViG架构的灵活性。其中,最大相对卷积在计算量和精度之间实现了最佳的权衡。 直接利用图卷积进行图像分类的效果很差,可以通过添加更多的特征转换,如引入FC和FFN不断提高准确性。 太少的邻居结点会降低信息交流,太多会导致过度平滑。当邻居节点的数量在9-15的范围时表现较好。 头的数量 h=4时,计算量和精度可以最好平衡。 ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:4:2","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["深度学习"],"content":"3.3 可视化 为了更好地理解本文的ViG模型是如何工作的,作者可视化了构建的图结构,展示了两个不同深度的样本的图。五角星是中心节点,相同颜色的节点是其邻居。 可以看到,在浅层,邻居节点往往是根据低层次、局部特征来选择的,如颜色和纹理;在深层层中,中心节点的邻居更具语义性,属于同一类别。而本文的ViG网络可以通过其内容和语义表征逐渐将节点联系起来,并帮助更好地识别物体。 参考资料: 从图(Graph)到图卷积(Graph Convolution):漫谈图神经网络模型 (一) 图卷积神经网络(GCN) GRAPH CONVOLUTIONAL NETWORKS 不用像素当做节点的原因:会导致节点过多 ↩︎ 最后加上 $X$​ 是残差连接,为了避免过拟合。 ↩︎ FLOPs:浮点运算数,可以用来衡量算法/模型的复杂度。 ↩︎ E是FNN中的隐藏维度 ↩︎ ","date":"2022-10-17","objectID":"/an-image-is-worth-graph-of-nodes/:4:3","tags":["深度学习","GNN"],"title":"Vision GNN: An Image is Worth Graph of Nodes","uri":"/an-image-is-worth-graph-of-nodes/"},{"categories":["Go"],"content":"1 结构 go run helloworld.go:执行Go代码 go build helloworld.go:编译生成二进制文件 ./helloworld:运行 import 声明必须跟在文件的 package 声明之后 Go 语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句 函数的左括号 { 必须和 func 函数声明在同一行上,且位于末尾,不能独占一行 在表达式 x+y 中,可在 + 后换行,不能在 + 前换行 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:1:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"2 基础语法 //格式化字符串 var stockcode = 123 var enddate = \"2020-12-31\" var url = \"Code=%d\u0026endDate=%s\" var target_url = fmt.Sprintf(url, stockcode, enddate) fmt.Println(target_url) ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:2:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"3 语言类型 布尔型 数字型 整形:int uint 浮点型:float complex 字符串 派生类型 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:3:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"4 变量 变量声明 var identifier type(指定变量类型,如果没有初始化,则变量默认为零值 var v_name = value(根据值自行判断变量类型 v_name := value(只能在函数体中出现 // 这种因式分解关键字的写法一般用于声明全局变量 var ( vname1 v_type1 vname2 v_type2 ) 局部变量不允许声明但不使用,全局变量可以 a, b = b, a (简单交换2个变量 _:空白标识符,也用于被抛弃值 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:4:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"5 常量 const identifier [type] = value //用作枚举 const ( Unknown = 0 Female = 1 Male = 2 ) 常量可以用len(), cap(), unsafe.Sizeof()函数计算表达式的值(必须是内置函数 iota 在const关键字出现时将被重置为0,const中每新增一行常量声明将使 iota 计数一次 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:5:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"6 条件语句 switch 匹配项后面也不需要再加 break fallthrough 执行后面的case case后面是类型不被局限于常量或整数,可以加多个,必须类型相同 func main() { var grade string = \"B\" var marks int = 90 switch marks { case 90: grade = \"A\" case 80,70: grade = \"B\" default: grade = \"D\" } switch { case grade == \"A\": fmt.Println(\"youxiu\") case grade == \"B\", grade == \"C\": fmt.Println(\"lianghao\") default: fmt.Println(\"cha\") } } type switch 判断某个 interface 变量中实际存储的变量类型 select 通信的 switch 语句 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:6:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"7 循环语句 for循环 for init; condition; post { } for condition { } for { } //range格式可以对 slice、map、数组、字符串等进行迭代循环 for key, value := range oldMap { newMap[key] = value } for key := range oldMap for _, value := range oldMap 在多重循环中,可以用标号 label 标出想 break 的循环 在多重循环中,可以用标号 label 标出想 continue 的循环 goto 语句可以无条件地转移到过程中指定的行 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:7:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"8 函数 func function_name( [parameter list] ) [return_types] { 函数体 } 函数可作为实参 匿名函数,可作为闭包 //方法 func (variable_name variable_data_type) function_name() [return_type]{ /* 函数体*/ } ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:8:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"9 变量作用域 局部变量:作用域只在函数体内 全局变量:整个包甚至外部包(被导出后)使用 全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:9:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"10 数组 var variable_name [SIZE] variable_type 可以使用 ... 代替数组的长度 // 将索引为 1 和 3 的元素初始化 balance := [...]float32{1:2.0,3:7.0} 多维数组 使用 append() 函数向空的二维数组添加两行一维数组 //多维数组 var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type 可以创建各个维度元素数量不一致的多维数组 //向函数传递数组 void myFunction(param [10]int) { ... } ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:10:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"11 指针 var var_name *var-type 指针数组 var ptr [MAX]*int; 指向指针的指针 var ptr **int; ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:11:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"12 结构体 //定义结构体 type struct_variable_type struct { member definition ... member definition } //声明变量 variable_name := structure_variable_type {value1, value2...valuen} variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen} 访问结构体:结构体.成员名 结构体作为函数参数 结构体指针 //声明 var struct_pointer *Books 结构体指针用 . 访问结构体成员 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:12:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"13 切片(Slice) var identifier []type //定义切片 var slice1 []type = make([]type, len) //创建切片 make([]T, length, capacity) //capacity指定容量,为可选参数 s :=[] int {1,2,3 } //直接初始化切片 s := arr[:] //初始化切片 s,是数组 arr 的引用 s := arr[startIndex:endIndex] //将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片 len() 方法获取长度 cap() 可以测量切片最长可以达到多少 空切片(nil):切片未初始化,默认为nil,长度为0 copy() 方法拷贝切片 append() 方法向切片追加新元素 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:13:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"14 范围(range) 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素 在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对 //读取key,value for key, value := range oldMap { newMap[key] = value } //只读取key for key := range oldMap //只读取value for _, value := range oldMap range也可以用来枚举 Unicode 字符串 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:14:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"15 集合(Map) 无序的键值对的集合 /* 声明变量,默认 map 是 nil */ var map_variable map[key_data_type]value_data_type /* 使用 make 函数 */ map_variable := make(map[key_data_type]value_data_type) delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:15:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"16 接口 /* 定义接口 */ type interface_name interface { method_name1 [return_type] method_name2 [return_type] ... method_namen [return_type] } /* 定义结构体 */ type struct_name struct { /* variables */ } /* 实现接口方法 */ func (struct_name_variable struct_name) method_name1() [return_type] { /* 方法实现 */ } ... func (struct_name_variable struct_name) method_namen() [return_type] { /* 方法实现*/ } ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:16:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"17 错误处理 type error interface { Error() string } func Sqrt(f float64) (float64, error) { if f \u003c 0 { return 0, errors.New(\"math: square root of negative number\") } // 实现 } ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:17:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"},{"categories":["Go"],"content":"18 并发 //goroutine语法 go 函数名( 参数列表 ) 同一个程序中的所有 goroutine 共享同一个地址空间 通道(channel) 是用来传递数据的一个数据结构 通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯 操作符 \u003c- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道 //声明通道 ch := make(chan int) //设置发送缓冲区 ch := make(chan int, 100) //遍历通道 v, ok := \u003c-ch //关闭通道 cl 参考资料: Go语言教程 ","date":"2022-10-16","objectID":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/:18:0","tags":["Go"],"title":"Go基本语法","uri":"/go%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"}] \ No newline at end of file diff --git a/index.xml b/index.xml index b8ced7c..2760311 100644 --- a/index.xml +++ b/index.xml @@ -27,7 +27,6 @@ https://imcaicai.github.io/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/ diff --git a/posts/index.xml b/posts/index.xml index b4c942d..1082ad0 100644 --- a/posts/index.xml +++ b/posts/index.xml @@ -25,7 +25,6 @@ https://imcaicai.github.io/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/ diff --git a/tags/c++/index.xml b/tags/c++/index.xml index dd6793f..25d504d 100644 --- a/tags/c++/index.xml +++ b/tags/c++/index.xml @@ -92,8 +92,12 @@ v1 容器开始时就有 20 个元素,它们的默认初始值都为 0。圆 Tue, 14 Mar 2023 17:15:09 +0800 菜菜 https://imcaicai.github.io/csp202209/ - + 【CSP】202212题解 @@ -101,9 +105,9 @@ v1 容器开始时就有 20 个元素,它们的默认初始值都为 0。圆 Tue, 14 Mar 2023 17:15:01 +0800 菜菜 https://imcaicai.github.io/csp202212/ - + 【STL】map容器用法 diff --git a/tags/csp/index.xml b/tags/csp/index.xml index 89e41ec..76b7032 100644 --- a/tags/csp/index.xml +++ b/tags/csp/index.xml @@ -34,8 +34,12 @@ Tue, 14 Mar 2023 17:15:09 +0800 菜菜 https://imcaicai.github.io/csp202209/ - + 【CSP】202212题解 @@ -43,9 +47,9 @@ Tue, 14 Mar 2023 17:15:01 +0800 菜菜 https://imcaicai.github.io/csp202212/ - + diff --git a/tags/labuladong/index.xml b/tags/labuladong/index.xml index b466217..3769598 100644 --- a/tags/labuladong/index.xml +++ b/tags/labuladong/index.xml @@ -11,7 +11,6 @@ https://imcaicai.github.io/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/ diff --git "a/tags/\345\212\250\346\200\201\350\247\204\345\210\222/index.xml" "b/tags/\345\212\250\346\200\201\350\247\204\345\210\222/index.xml" index 8c00afc..858c56b 100644 --- "a/tags/\345\212\250\346\200\201\350\247\204\345\210\222/index.xml" +++ "b/tags/\345\212\250\346\200\201\350\247\204\345\210\222/index.xml" @@ -11,7 +11,6 @@ https://imcaicai.github.io/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/ diff --git "a/tags/\347\256\227\346\263\225/index.xml" "b/tags/\347\256\227\346\263\225/index.xml" index e05b4d3..c21a797 100644 --- "a/tags/\347\256\227\346\263\225/index.xml" +++ "b/tags/\347\256\227\346\263\225/index.xml" @@ -11,7 +11,6 @@ https://imcaicai.github.io/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6/ diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\345\212\250\346\200\201\350\247\204\345\210\222\346\240\270\345\277\203\346\241\206\346\236\266/index.html" "b/\345\212\250\346\200\201\350\247\204\345\210\222\345\212\250\346\200\201\350\247\204\345\210\222\346\240\270\345\277\203\346\241\206\346\236\266/index.html" index cf72805..3afd2f2 100644 --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\345\212\250\346\200\201\350\247\204\345\210\222\346\240\270\345\277\203\346\241\206\346\236\266/index.html" +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\345\212\250\346\200\201\350\247\204\345\210\222\346\240\270\345\277\203\346\241\206\346\236\266/index.html" @@ -7,7 +7,6 @@ 【动态规划】动态规划核心框架 - 菜菜的秘密花园 @@ -20,7 +19,6 @@ @@ -34,7 +32,7 @@ "mainEntityOfPage": { "@type": "WebPage", "@id": "https:\/\/imcaicai.github.io\/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6\/" - },"image": ["https:\/\/imcaicai.github.io\/img\/avatar.jpg"],"genre": "posts","keywords": "labuladong, 算法, 动态规划","wordcount": 784 , + },"image": ["https:\/\/imcaicai.github.io\/img\/avatar.jpg"],"genre": "posts","keywords": "labuladong, 算法, 动态规划","wordcount": 783 , "url": "https:\/\/imcaicai.github.io\/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%A0%B8%E5%BF%83%E6%A1%86%E6%9E%B6\/","datePublished": "2023-03-18T00:04:04+08:00","dateModified": "2023-03-18T00:04:04+08:00","license": "This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.","publisher": { "@type": "Organization", "name": "xxxx","logo": "https:\/\/imcaicai.github.io\/img\/avatar.jpg"},"author": { @@ -128,7 +126,7 @@

    Contents

    【动态规划】动态规划核心框架

    🔴 【动态规划三要素】重叠子问题、最优子结构、状态转移方程

    🟢 【思维框架】明确 base case → 明确「状态」→ 明确「选择」 → 定义 dp 数组/函数的含义。

    -

    🔵

     1
    diff --git "a/\345\212\250\346\200\201\350\247\204\345\210\222\345\212\250\346\200\201\350\247\204\345\210\222\346\240\270\345\277\203\346\241\206\346\236\266/index.md" "b/\345\212\250\346\200\201\350\247\204\345\210\222\345\212\250\346\200\201\350\247\204\345\210\222\346\240\270\345\277\203\346\241\206\346\236\266/index.md"
    index 8fd43a3..cecda77 100644
    --- "a/\345\212\250\346\200\201\350\247\204\345\210\222\345\212\250\346\200\201\350\247\204\345\210\222\346\240\270\345\277\203\346\241\206\346\236\266/index.md"
    +++ "b/\345\212\250\346\200\201\350\247\204\345\210\222\345\212\250\346\200\201\350\247\204\345\210\222\346\240\270\345\277\203\346\241\206\346\236\266/index.md"
    @@ -5,8 +5,6 @@
     
     🟢 **【思维框架】明确 base case → 明确「状态」→ 明确「选择」 → 定义 dp 数组/函数的含义。**
     
    -🔵
    -
     ```Python
     # ⾃顶向下递归的动态规划
     def dp(状态1, 状态2, ...):