在说断点续传之前，先说一下文件上传的原理，因为断点续传的应用场景就是文件上传。文件上传客户端发送上传文件请求服务器收到请求之后解析请求头和请求体，获取上传的数据并进行验证（已经上传的文件不重复上传）服务器端将分片上传的文件存在临时目录中，所有分片上传后再进行合并和重命名最后将文件存储在指定的目录断点续传文件传输过程中，如果出现传输中断或者失败的情况，可以从中断的地方继续传输，不用重新开始传输。断点续传有很多种实现方式，这里推荐b站一个用Java实现多线程断点续传的视频<4.java多线程-实现断点续传_哔哩哔哩_bilibili>在实际应用时我选择用Redis实现断点续传大致的思路：前端计算上传文件的MD5值，如果Redis中由对应的MD5值则说明秒传成功后端通过Redis记录文件分片的情况（Redis中使用Set记录哪些分片已经上传，已经上传过的分片会被跳过），前端开始上传之后，每次上传成功都要记录在Redis中，续传之后前端重新计算MD5值，重新开始上传Redis中记录缺失的分片。当所有分片都上传完成并且合并成文件之后，删除Redis中对应的分片记录。以上思路只适用于单线程，如果要使用多线程方式实现，需要考虑更多的问题~代码实现 @Transactional(rollbackFor = Exception.class) public boolean uploadFile(MultipartFile file, String fileName, String fileMD5, String fileId, HttpServletRequest request, String redisKey) { try { // 参数校验 if (file == null || file.isEmpty()) { throw new IllegalArgumentException("文件不能为空"); } // 如果fileId为空，则生成一个随机的fileId if (StringUtils.isEmpty(fileId)) { fileId = RandomStringUtils.random(10); } // 从请求头中获取分片信息 int chunkNumber = 0; int totalChunks = 1; long chunkSize = file.getSize(); long totalSize = file.getSize(); String chunkMD5 = null; try { chunkNumber = Integer.parseInt(request.getHeader("Chunk-Number")); totalChunks = Integer.parseInt(request.getHeader("Total-Chunks")); chunkSize = Long.parseLong(request.getHeader("Chunk-Size")); totalSize = Long.parseLong(request.getHeader("Total-Size")); chunkMD5 = request.getHeader("Chunk-MD5"); } catch (Exception e) { log.info("未获取到分片信息"); } // 创建临时目录 String tempDir = System.getProperty("java.io.tmpdir") + "/upload/" + fileMD5 + "/"; File tempFileDir = new File(tempDir); if (!tempFileDir.exists()) { tempFileDir.mkdirs(); } // 秒传检查 File finalFile = new File("/final/path/" + fileId + "_" + fileName); if (finalFile.exists() && finalFile.length() == totalSize) { log.info("文件已存在，秒传成功"); return true; } // 处理分片上传 File chunkFile = new File(tempDir + "chunk_" + chunkNumber); // 断点续传检查 - 结合Redis Boolean chunkExistsInRedis = redisTemplate.opsForSet().isMember( String.format(CHUNK_UPLOAD_KEY, fileMD5), String.valueOf(chunkNumber)); if (chunkFile.exists() && chunkFile.length() == chunkSize && Boolean.TRUE.equals(chunkExistsInRedis)) { log.info("分片 {} 已存在，跳过上传", chunkNumber); } else { // 保存分片文件 File tempChunkFile = new File(tempDir + "chunk_" + chunkNumber + ".tmp"); file.transferTo(tempChunkFile); // 分片MD5校验 if (chunkMD5 != null && !chunkMD5.isEmpty()) { try (FileInputStream fis = new FileInputStream(tempChunkFile)) { MessageDigest digest = MessageDigest.getInstance("MD5"); byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { digest.update(buffer, 0, bytesRead); } byte[] md5Bytes = digest.digest(); StringBuilder sb = new StringBuilder(); for (byte b : md5Bytes) { sb.append(String.format("%02x", b)); } String actualChunkMD5 = sb.toString(); if (!chunkMD5.equals(actualChunkMD5)) { tempChunkFile.delete(); throw new RuntimeException("分片MD5校验失败"); } } } // 重命名临时文件 if (!tempChunkFile.renameTo(chunkFile)) { // 重命名失败时复制文件 try (FileInputStream fis = new FileInputStream(tempChunkFile); FileOutputStream fos = new FileOutputStream(chunkFile)) { byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { fos.write(buffer, 0, bytesRead); } } tempChunkFile.delete(); } // 将已上传的分片信息记录到Redis redisTemplate.opsForSet().add( String.format(CHUNK_UPLOAD_KEY, fileMD5), String.valueOf(chunkNumber)); // 设置过期时间（例如24小时） redisTemplate.expire( String.format(CHUNK_UPLOAD_KEY, fileMD5), 24, TimeUnit.HOURS); } // 检查是否所有分片都已上传完成 - 使用Redis记录的信息 long uploadedChunksCount = redisTemplate.opsForSet().size( String.format(CHUNK_UPLOAD_KEY, fileMD5)); boolean allChunksUploaded = uploadedChunksCount == totalChunks; // 如果所有分片都已上传，合并文件 if (allChunksUploaded) { // 创建合并后的文件 File mergedFile = new File(tempDir + "merged_" + fileMD5 + "_" + fileName); try (FileOutputStream fos = new FileOutputStream(mergedFile, true)) { for (int i = 0; i < totalChunks; i++) { File currentChunk = new File(tempDir + "chunk_" + i); try (FileInputStream fis = new FileInputStream(currentChunk)) { byte[] buffer = new byte[1024 * 8]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { fos.write(buffer, 0, bytesRead); } } currentChunk.delete(); } } // 验证合并文件的MD5 try (FileInputStream fis = new FileInputStream(mergedFile)) { MessageDigest digest = MessageDigest.getInstance("MD5"); byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { digest.update(buffer, 0, bytesRead); } byte[] md5Bytes = digest.digest(); StringBuilder sb = new StringBuilder(); for (byte b : md5Bytes) { sb.append(String.format("%02x", b)); } String actualFileMD5 = sb.toString(); if (!fileMD5.equals(actualFileMD5)) { throw new RuntimeException("文件MD5校验失败"); } } // 验证文件大小 if (mergedFile.length() != totalSize) { throw new RuntimeException("文件大小不匹配"); } // 保存到最终位置 File finalDir = new File("/final/path/"); if (!finalDir.exists()) { finalDir.mkdirs(); } if (finalFile.exists()) { finalFile.delete(); } Files.move(mergedFile.toPath(), finalFile.toPath(), StandardCopyOption.REPLACE_EXISTING); // 清理Redis中的分片记录 redisTemplate.delete(String.format(CHUNK_UPLOAD_KEY, fileMD5)); // 清理临时目录 File[] tempFiles = tempFileDir.listFiles(); if (tempFiles != null) { for (File tempFile : tempFiles) { tempFile.delete(); } } tempFileDir.delete(); // TODO 更新数据库记录 log.info("文件上传完成: {}", fileName); return true; } return true; } catch (Exception e) { log.error("文件上传失败", e); return false; } }
简单复习一下有关质数的知识质数的定义在大于1的自然数中，因数只有1和它本身的自然数。由这个定义可以得到，1不是质数，2是最小的质数。判断质数试除法试除法是最快能想到的方法，这里的代码做了一点优化，即从2遍历到sqrt(n)而不是n-1。这是因为：如果n有一个大于sqrt(n)的因子，那么它必定有一个小于sqrt(n)的因子。因此，如果我们在2到sqrt(n)之间没有找到因子，那么大于sqrt(n)的范围内也不会找到因子。在遍历时需要判断i*i<=x，为了防止溢出，这里的写法是i<=x/i#include<iostream> using namespace std; bool is_prime(int x) { if(x<2) return false; else { for(int i=2;i<=x/i;i++)//从2遍历sqrt(x) if(x%i==0) return false; } return true; } int main() { int n,x; cin>>n; int cnt=0; while(n--) { cin>>x; if(is_prime(x)) cnt++; } return cnt; }质数筛质数定理：在1~n之间有n/logn个质数朴素筛法原理：把所有要判断的数列出来，把每一个数的倍数删掉对任意一个数P来说，如果它没有被删掉，则2~P-1都不是它的约数时间复杂度O(nlogn)#include<iostream> #include<algorithm> using namespace std; const int N=1000010; int primes[N],cnt; bool st[N]; void get_primes(int n) { for(int i=2;i<=n;i++) { //如果这个数没有被筛过，说明它是质数 if(!st[i]) { primes[cnt++]=n; } //把每一个数的倍数删掉 for(int j=i+i;j<=n;j+=i) st[j]=true; } } int main() { int n; cin >> n; get_primes(n); cout << cnt << endl; return 0; }埃氏筛法优化：在朴素筛法的基础上，只删除所有质数的倍数//埃氏筛法 #include<iostream> using namespace std; const int N=1000010; int primes[N],cnt; bool st[N]; void get_primes(int n){ for(int i=2;i<=n;i++) { if(!st[i]) { primes[cnt++]=i; for(int j=i*i;j<=n;j+=i) st[j]=true; //可以用质数把所有的合数都筛掉； } } } int main() { int n; cin >> n; get_primes(n); cout << cnt << endl; return 0; }线性筛法原理：从小到大枚举所有质数,筛掉这些质数的倍数对于每一个合数n，只会被它的最小质因子筛掉i % primes[j]==0:primes[j]一定是i的最小质因子，primes[j]也一定是primes[j]*i的最小质因子i % primes[j]≠0:primes[j]一定小于i的所有质因子，primes[j]也一定是primes[j]*i的最小质因子//线性筛法 #include<iostream> using namespace std; const int N=10000010; int primes[N],cnt; bool st[N]; void get_prime(int n) { for(int i=2;i<n;i++) { if(!st[i]) primes[cnt++]=i; for(int j=0;primes[j]<n/i;j++) { st[primes[j]*i]=true; if(i%primes[j]==0)break;//primes[j]一定是i的最小质因子 } } } int main() { int n; cin>>n; get_prime(n); cout<<cnt<<endl; }分解质因数对于一个合数n，把它分解成$p_{1}^{n_{1}}*p_{2}^{n_{2}}$的形式并输出#include<iostream> #include<algorithm> using namespace std; void divide(int n) { for(int i=2;i<=n/i;i++) if(n%i==0) { int s=0; while(n%i==0)//i一定是质数 { n/=i; s++; } cout<<i<<' '<<s<<endl; } if(n>1)cout<<n<<<" "<1<<endl; cout<<endl; } int main() { int n; cin>>n; while(n--) { int x; cin>>x; divide(x); } }
分享一首从中学时期我就很喜欢的一首诗，希望未来的某一天当我回忆往事时，不会后悔自己所做的选择。未选择的路 弗罗斯特黄色的树林里分出两条路，可惜我不能同时去涉足，我在那路口久久伫立，我向着一条路极目望去，直到它消失在丛林深处。但我却选了另外一条路，它荒草萋萋，十分幽寂，显得更诱人，更美丽；虽然在这条小路上，很少留下旅人的足迹。那天清晨落叶满地，两条路都未经脚印污染。啊，留下一条路等改日再见！但我知道路径延绵无尽头，恐怕我难以再回返。也许多少年后在某个地方，我将轻声叹息将往事回顾：一片树林里分出两条路——而我选择了人迹更少的一条，从此决定了我一生的道路。
做了一个小的实践，用C语言实现了128比特版本的AES算法。文章里面有很多公式，我实在调不好LaTex公式的渲染了，如果阅读起来不方便请见谅。1.问题背景及相关知识(1)算法提出背景1976年，“公钥密码”的概念被提出。在此类密码中，公钥用于加密，私钥用于解密，加密解密使用两种不同的密钥。随后1978年发布的基于数论方法构造的RSA算法成为之后几十年密码学的研究方向。针对RSA和DES算法的研究和计算机能力的不断提高，公钥密码体制的安全性逐渐受到威胁。数据加密标准DES不能满足官方和商业应用的安全要求，因此美国国家标准技术研究所NIST征集取代DES的新一代加密标准。经过五年的征集筛选，NIST最终将Rijndael算法确立为新的有效标准，以应对各种针对于RSA，DES算法的先进的密码分析技术，即高级加密标准AES。(2)相关数学知识a.有限域有限域又称为伽瓦罗域，是具有有限个元素的域，集合中元素的个数称为域的阶。M阶域存在当且仅当m是某素数的幂，即存在某个整数$n$和素数$p$，使得$$\begin{align*}\text{m = p}^{\text{n}}\end{align*}$$。$p$称为有限域的特征。b.扩展域如果有限域的阶不是素数，则这样的有限域内的加法和乘法运算就不能用模整数加法和整数乘法模$p$表示。$m>1$的域称为扩展域。在扩展域$$\begin{align*}\text{GF}(2^{\text{m}})\end{align*}$$中元素并不是用整数表示的，而是用系数为$GF(2)$中元素的多项式表示。这个多项式最大的度为$m-1$扩展域内的运算假设$$A(x),B(x) \in \text{GF}(2^{\text{m}})$$扩展域内的加法$$ \begin{align*}C(x) = A(x) + B(x) = \sum_{i=0}^{m-1} c_i x^i \quad \quad c_i \equiv (a_i + b_i) \text{mod} 2\end{align*}$$扩展域内的减法$$\begin{align*}C(x) = A(x) - B(x) = \sum_{i=0}^{m-1} c_i x^i \quad c_i \equiv (a_i - b_i) \text{mod} 2 \equiv (a_i + b_i) \text{mod} 2\end{align*}$$扩展域内的乘法扩展域乘法主要在列混合步骤中使用，我们要将扩展域中的两个元素用多项式形式展开，然后使用多项式乘法规则将两个多项式相乘。$$\begin{align*}C(x) = A(x) \cdot B(x) \text{m} o dP(x)\end{align*}$$通常在多项式乘法中C(x)的度会大于m-1，因此需要对其进行化简。扩展域中进行化简的操作为：将这两个多项式相乘的结果除以一个不可约多项式，最后的结果就是最后的余数。AES算法中使用的不可约多项式为:$$\begin{align*}P(x) = x^8 + x^4 + x^3 + x + 1\end{align*}$$2.算法原理AES算法作为一种对称加密算法，加密/解密时通信双方使用单一密钥。而对称密码算法根据对明文消息加密方式的不同可分为两大类，分组密码和流密码。AES算法属于分组密码算法，它的输入分组、输出分组以及加密/解密过程中的中间分组都是128比特。密钥的长度K可以为128，192或256比特，本次实践中实现的是128比特的版本。AES加密算法的数据处理单元是字节，128比特的分组信息被分成16个字节按顺序被复制到一个的矩阵中，这个矩阵称为状态，AES所有变换都是基于状态的变换。AES的所有变换操作，包括字节替换、行变换、列混合和轮密钥添加，均围绕这个状态矩阵展开。这种矩阵化的表示方式不仅便于数学描述，也有利于硬件实现时的并行处理。轮函数是AES加密的核心组件，通过多轮迭代实现数据的混淆（Confusion）与扩散（Diffusion）。在轮函数的每一轮迭代中包含四步变换，分别是：字节替换，行移位，列混合（最后一轮没有列混合）以及轮密钥添加，其作用即使通过简单的非线性变换、混合函数变换，将字节代换运算产生的非线性扩散达到充分混合，在每轮迭代中引入不同的密钥，这样就能以最简单的运算得到最好的加密效果，实现加密的有效性。AES算法流程图如下：(1)字节替换a. S盒的构造首先对于任意输入的字节求逆元，如果x=0x00，则其逆元定义为0x00，否则在$GF(2^8)$上计算x的逆元。在域上$a \otimes b = 1$$\otimes$表示扩展域内的乘法，则称b为a的逆元对x的逆元应用在$GF(2)$上的仿射变换$\begin{align*}Y = AX^{-1} + b\end{align*}$得到Y，其中A是一个变换矩阵，b是一个常数A是由循环矩阵构成的，具体内容为经过上述变换之后得到的结果$Y$就是S盒中对应于$X$的输出b.用S盒进行字节替换操作明文中的每一个字节都会被另一个字节替换，替换规则来源于一个预先计算好的查找表，称为S盒。S盒是一个拥有256个字节元素的数组，可以定义为一个16*16的二维数组。具体替换规则如下：对于每个元素$A$，从存储角度都看作是一个八位的二进制数。算出前四位所代表的十六进制数$x$和后四位所代表的十六进制数$y$，在S盒中找到对应的$x$行$y$列的元素替换原来的字节(2)行变换行变换在状态矩阵State的每一行之间进行，具体操作为：对每一行及逆行循环移位，移动位数以字节为单位，第一行不变，第二行循环左移一个字节，第三行循环左移两个字节，第四行循环左移三个字节。行变换是一种线性变换，目的是使密码信息充分混乱，提高非线性度。(3)列混合列混合是对状态矩阵State的每一列进行的一种线性变换，State的每一列有4个字节，即一个字$\{ b_0, b_1, b_2, b_3 \}$。将每一列视为$GF(2^{8})$上的一个多项式$b(x) = b_3 x^3 + b_2 x^2 + b_1 x + b_0$，然后与一个固定的多项式$a(x)$进行模乘法，然后将所得的结果进行取模运算，模值为$x^{4}+1$。在$GF(2^8)$上$x^{4}+1$不是不可约多项式，因此，用$b(x)$乘以任意一个四项式的运算不一定是可逆的，于是在AES中将$a(x)$设置成一个固定的值，以确保可以进行求逆运算。$a(x)$的表达式为:$a(x) = \{03\} x^3 + \{01\} x^2 + \{01\} x + \{02\}$。(4)轮密钥加将状态矩阵State中的每个字节与对应轮密钥的字节进行简单的按位异或操作，得到新一轮的密钥。这样可以通过密钥扩展算法派生出来一系列子密钥，每一轮使用的子密钥都不相同。(5)解密对AES算法加密的密文进行解密，只需进行加密操作的逆过程即可。下面时比较重要的环节的算法原理。a. 逆S盒的构造对于每个字节$x$，先对$x$的进行逆仿射变换。仿射变换是$Y = AX^{-1} + b$，而逆仿射变换是$E = A^{-1} (X + b)$得到结果$E$，其中$A^{-1}$是仿射变换中矩阵$A$的逆矩阵，$b$依然是常数63。然后对逆仿射变换的结果$E$求逆元，求逆元得到的结果就是字节$x$对应的逆S盒中的元素。3.设计和实现过程(1)字节替换在字节替换前直接定义好S盒和逆S盒，方便后续直接使用直接将S盒、逆S盒定义为16*16的char型数组(2)行变换行变换只需要简单的移位操作即可。(3)列混合在调用列混合函数之前，先定义了扩展域上的乘法函数X2time为有限域上的乘2乘法的实现，X3time为有限域上乘3乘法的实现,以此类推定义好有限域上的乘法运算函数，方便在列混合时直接调用。(4)轮密钥加将状态矩阵State中的每个字节与对应轮密钥的字节进行简单的按位异或操作。(5)解密过程解密过程就是加密的逆过程。定义逆行移位、逆字节替换、逆列混合等函数。逆字节替换和加密时的字节替换类似，我们使用预先算好的逆S盒进行逆字节替换的操作。而逆行移位、逆列混合则是和加密时的操作完全相反即可。(6)主函数设计算法有两种执行模式，分别是使用预定义的NIST测试向量进行测试，以及用户自定义输入。在用户自定义输入的情况下，用户可以选择加密或者解密模式，两种模式均需要用户输入密钥以及明文/密文进行加密/解密。用户输入的密钥和明文/密文格式都是十六进制，每两位之间需要用空格间隔开，输入格式不正确时程序不会执行，并且输出错误提示信息。完整代码如下//头文件 #pragma once #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #ifdef __cplusplus extern "C"{ #endif void encryptAES(unsigned char* out, const unsigned char* in, const unsigned char* key, const int keyLen); void decryptAES(unsigned char* out, const unsigned char* in, const unsigned char* key, const int keyLen); #ifdef __cplusplus } #endif#include "aes.h" // 在Windows系统中，为了正确显示中文，需要添加这个头文件 #ifdef _WIN32 #include <windows.h> #endif // 添加延迟函数所需的头文件 #ifdef _WIN32 #include <windows.h> #else #include <unistd.h> #endif // 延迟函数实现 void delay(int seconds) { #ifdef _WIN32 Sleep(seconds * 1000); #else sleep(seconds); #endif } const unsigned char sBox[16][16] = { 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 }; const unsigned char inv_sBox[16][16] = { 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d }; const unsigned char Rcon[16] = { 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0xd8, 0xab, 0x4d, 0x9a, 0x2f }; // 轮常量表 void SubBytes(unsigned char *in, const unsigned char table[16][16]) { // 字节替换 for (int i = 0; i < 16; i++) in[i] = table[in[i] >> 4][in[i] & 0x0F]; } void ShiftRows(unsigned char *in) { // 第0行不变 // 第1行左移1位 unsigned char temp = in[1]; in[1] = in[5]; in[5] = in[9]; in[9] = in[13]; in[13] = temp; // 第2行左移2位 temp = in[2]; in[2] = in[10]; in[10] = temp; temp = in[6]; in[6] = in[14]; in[14] = temp; // 第3行左移3位 temp = in[3]; in[3] = in[15]; in[15] = in[11]; in[11] = in[7]; in[7] = temp; } void InvShiftRows(unsigned char *in) { // 第0行不变 // 第1行右移1位 (相当于左移3位) unsigned char temp = in[13]; in[13] = in[9]; in[9] = in[5]; in[5] = in[1]; in[1] = temp; // 第2行右移2位 (相当于左移2位) temp = in[2]; in[2] = in[10]; in[10] = temp; temp = in[6]; in[6] = in[14]; in[14] = temp; // 第3行右移3位 (相当于左移1位) temp = in[7]; in[7] = in[11]; in[11] = in[15]; in[15] = in[3]; in[3] = temp; } unsigned char x2time(unsigned char a) { // 有限域*2乘法 //如果a的最高位为1，则异或0x1b，否则异或0x00 unsigned char res = (a << 1) ^ ((a & 0x80) ? 0x1b : 0x00); return res; } unsigned char x3time(unsigned char a) { // 3:0011 return (x2time(a) ^ a); } unsigned char x4time(unsigned char a) { // 4:0100 return (x2time(x2time(a))); } unsigned char x8time(unsigned char a) { // 8:1000 return (x2time(x2time(x2time(a)))); } unsigned char x9time(unsigned char a) { // 9:1001 return (x8time(a) ^ a); } unsigned char xBtime(unsigned char a) { // B:1011 return (x8time(a) ^ x3time(a)); } unsigned char xDtime(unsigned char a) { // D:1101 return (x8time(a) ^ x4time(a) ^ a); } unsigned char xEtime(unsigned char a) { // E:1110 return (x8time(a) ^ x4time(a) ^ x2time(a)); } void MixColumn(unsigned char *in) { // 列混合 unsigned char tmp[4] = {0}; for (int i = 0; i < 4; i++, in += 4) { tmp[0] = x2time(in[0]) ^ x3time(in[1]) ^ in[2] ^ in[3]; tmp[1] = in[0] ^ x2time(in[1]) ^ x3time(in[2]) ^ in[3]; tmp[2] = in[0] ^ in[1] ^ x2time(in[2]) ^ x3time(in[3]); tmp[3] = x3time(in[0]) ^ in[1] ^ in[2] ^ x2time(in[3]); memcpy(in, tmp, 4); } } void InvMixColumn(unsigned char *in) { // 逆列混合 unsigned char tmp[4] = {0}; for (int i = 0; i < 4; i++, in += 4) { tmp[0] = xEtime(in[0]) ^ xBtime(in[1]) ^ xDtime(in[2]) ^ x9time(in[3]); tmp[1] = x9time(in[0]) ^ xEtime(in[1]) ^ xBtime(in[2]) ^ xDtime(in[3]); tmp[2] = xDtime(in[0]) ^ x9time(in[1]) ^ xEtime(in[2]) ^ xBtime(in[3]); tmp[3] = xBtime(in[0]) ^ xDtime(in[1]) ^ x9time(in[2]) ^ xEtime(in[3]); memcpy(in, tmp, 4); } } void AddRoundKey(unsigned char *in, const unsigned char *key) { for (int i = 0; i < 16; i++) in[i] ^= key[i]; } void rotWord(unsigned char *in) { // 将一个字进行左移循环一个单位 unsigned char tmp = in[0]; memcpy(in, in + 1, 3); in[3] = tmp; } void subWord(unsigned char *in) { // 每一个字进行字节替换 for (int i = 0; i < 4; i++) in[i] = sBox[in[i] >> 4][in[i] & 0x0F]; } // 存储扩展后的轮密钥数组 static unsigned char roundKey[240] = {0}; void SetKey(const unsigned char *key, const int Nk) { // Nr为加密轮数，计算公式为Nk + 6 int Nr = Nk + 6; // 将初始密钥复制到轮密钥数组的起始位置 // 每字4字节，所以复制4*Nk个字节 memcpy(roundKey, key, 4 * Nk); // 生成剩余的轮密钥 // 总共需要生成(Nr+1)*4个字的轮密钥 for (int i = Nk; i < (Nr + 1) * 4; i++) { unsigned char buffer[4] = {0}; // 复制前一个字到缓冲区 memcpy(buffer, roundKey + (i - 1) * 4, 4); if (i % Nk == 0) { // 当i是Nk的倍数时 // 循环左移一个字节 rotWord(buffer); // 字节替换 subWord(buffer); buffer[0] ^= Rcon[i / Nk]; } // 与前Nk个字异或生成新字 for (int j = 0; j < 4; j++) { roundKey[4 * i + j] = buffer[j] ^ roundKey[(i - Nk) * 4 + j]; } } } void encryptAES(unsigned char *out, const unsigned char *in, const unsigned char *key, const int keyLen) { int Nk = keyLen >> 2; int Nr = Nk + 6; SetKey(key, Nk); memcpy(out, in, 16); AddRoundKey(out, roundKey); for (int i = 1; i <= Nr; i++) { // 进行Nr轮变换，最后一个Nr轮不进行列混合 SubBytes(out, sBox); ShiftRows(out); if (i != Nr) MixColumn(out); AddRoundKey(out, roundKey + 16 * i); } } void decryptAES(unsigned char *out, const unsigned char *in, const unsigned char *key, const int keyLen) { int Nk = keyLen >> 2; int Nr = Nk + 6; SetKey(key, Nk); memcpy(out, in, 16); AddRoundKey(out, roundKey + Nr * 16); for (int i = Nr - 1; i >= 0; i--) { InvShiftRows(out); SubBytes(out, inv_sBox); AddRoundKey(out, roundKey + 16 * i); if (i != 0) InvMixColumn(out); } } int main() { #ifdef _WIN32 SetConsoleOutputCP(65001); SetConsoleCP(65001); #endif // 程序控制变量 char continueProgram = 'y'; // 控制是否继续执行程序 unsigned char key[16] = {0}; // 存储16字节密钥 unsigned char input[16] = {0}; // 存储输入数据(明文或密文) unsigned char output[16] = {0}; // 存储输出数据(密文或明文) int mode; // 操作模式(1-加密, 2-解密) int useTestVector; // 输入方式选择(1-NIST测试向量, 2-自定义输入) // 预定义NIST 128位AES测试向量 - 用于验证AES实现的正确性 const unsigned char nistKey[16] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }; // 明文 const unsigned char nistPlaintext[16] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff }; // 密文 const unsigned char nistCiphertext[16] = { 0x69, 0xc4, 0xe0, 0xd8, 0x6a, 0x7b, 0x04, 0x30, 0xd8, 0xcd, 0xb7, 0x80, 0x70, 0xb4, 0xc5, 0x5a }; // 显示程序标题 printf("===== AES 128位加密工具 =====\n"); // 主循环 - 支持多次加密/解密操作 while (continueProgram == 'y' || continueProgram == 'Y') { // 选择使用测试向量还是自定义输入 printf("\n请选择输入方式:\n"); printf("1. 使用NIST测试向量\n"); printf("2. 自定义输入\n"); printf("请输入选择 (1/2): "); scanf("%d", &useTestVector); getchar(); if (useTestVector == 1) { // 使用NIST测试向量模式 memcpy(key, nistKey, 16); memcpy(input, nistPlaintext, 16); printf("已加载NIST测试向量:\n"); printf("密钥: "); for (int i = 0; i < 16; i++) { printf("%02x ", key[i]); } printf("\n"); // 执行加密操作 encryptAES(output, input, key, 16); // 输出密文并与标准密文比较 printf("加密结果 (十六进制): "); int match = 1; for (int i = 0; i < 16; i++) { printf("%02x ", output[i]); if (output[i] != nistCiphertext[i]) { match = 0; } } printf("\n"); if (match) { printf("加密结果与NIST标准密文匹配!\n"); } else { printf("加密结果与NIST标准密文不匹配!\n"); printf("标准密文: "); for (int i = 0; i < 16; i++) { printf("%02x ", nistCiphertext[i]); } printf("\n"); } // 执行解密测试 unsigned char decrypted[16] = {0}; decryptAES(decrypted, nistCiphertext, key, 16); // 输出解密结果并与原始明文比较 printf("解密结果 (十六进制): "); match = 1; for (int i = 0; i < 16; i++) { printf("%02x ", decrypted[i]); if (decrypted[i] != nistPlaintext[i]) { match = 0; } } printf("\n"); // 显示解密结果比较信息 if (match) { printf("解密结果与原始明文匹配!\n"); } else { printf("解密结果与原始明文不匹配!\n"); } } else if (useTestVector == 2) { // 自定义输入模式 // 选择加密或解密模式 printf("\n请选择操作模式:\n"); printf("1. 加密\n"); printf("2. 解密\n"); printf("请输入选择 (1/2): "); scanf("%d", &mode); getchar(); printf("请输入16字节密钥 (十六进制，每两位用空格分隔): "); char hexInput[64]; fgets(hexInput, sizeof(hexInput), stdin); hexInput[strcspn(hexInput, "\n")] = '\0'; // 去除换行符 // 解析密钥 int byteCount = 0; char *token = strtok(hexInput, " "); while (token != NULL && byteCount < 16) { if (strlen(token) == 2) { sscanf(token, "%2hhx", &key[byteCount]); byteCount++; } token = strtok(NULL, " "); } // 判断密钥格式是否正确 if (byteCount != 16) { printf("密钥格式错误，请确保输入了16个字节的十六进制值，每两位用空格分隔。\n"); continue; // 重新开始循环 } // 根据选择的模式处理输入 if (mode == 1) { // 加密模式 - 输入并解析明文 printf("请输入要加密的明文 (16字节，十六进制，每两位用空格分隔): "); fgets(hexInput, sizeof(hexInput), stdin); hexInput[strcspn(hexInput, "\n")] = '\0'; // 解析明文 byteCount = 0; token = strtok(hexInput, " "); while (token != NULL && byteCount < 16) { if (strlen(token) == 2) { sscanf(token, "%2hhx", &input[byteCount]); byteCount++; } token = strtok(NULL, " "); } // 验明文格式是否正确 if (byteCount != 16) { printf("明文格式错误，请确保输入了16个字节的十六进制值，每两位用空格分隔。\n"); continue; } // 执行加密操作 encryptAES(output, input, key, 16); // 输出密文结果 printf("加密结果 (十六进制): "); for (int i = 0; i < 16; i++) { printf("%02x ", output[i]); } printf("\n"); } else if (mode == 2) { // 解密模式 - 输入并解析密文 printf("请输入要解密的密文 (16字节，十六进制，每两位用空格分隔): "); fgets(hexInput, sizeof(hexInput), stdin); hexInput[strcspn(hexInput, "\n")] = '\0'; // 解析密文 byteCount = 0; token = strtok(hexInput, " "); while (token != NULL && byteCount < 16) { if (strlen(token) == 2) { sscanf(token, "%2hhx", &input[byteCount]); byteCount++; } token = strtok(NULL, " "); } // 判断密文格式是否正确，如果错误输出提示信息 if (byteCount != 16) { printf("密文格式错误，请确保输入了16个字节的十六进制值，每两位用空格分隔。\n"); continue; } // 执行解密操作 decryptAES(output, input, key, 16); printf("解密结果 (十六进制): "); for (int i = 0; i < 16; i++) { printf("%02x ", output[i]); } printf("\n"); } else { // 无效的模式选择 printf("无效的选择，请重新输入。\n"); continue; } } else { // 无效的输入方式选择 printf("无效的选择，请重新输入。\n"); continue; } // 询问是否继续执行程序 printf("\n是否继续? (y/n): "); continueProgram = getchar(); getchar(); // 吸收换行符 } // 程序退出提示 printf("\n程序已退出，感谢使用！\n"); return 0; }
最近写题目的时候发现一些排序算法我不太熟悉，简单记录一下。下面所有的算法都会把数组元素按照升序排序。插入排序插入排序的核心思想是：遍历未排序的序列，对未排序序列的元素插入到已排序的序列中。插入排序的步骤：从第一个元素开始，该元素可以认为已经被排序。取出下一个元素，在已经排序的元素序列中从后向前扫描。如果该元素（已排序）大于新元素，将该元素移到下一位置。重复步骤3，直到找到已排序的元素小于或者等于新元素的位置。将新元素插入到该位置后。重复步骤2~5。public void insertionSort(int[] arr) { if (arr == null || arr.length <= 1) { return; // 数组为空或只有一个元素时无需排序 } for (int i = 1; i < arr.length; i++) { int key = arr[i]; // key是当前需要插入的元素 int j = i - 1; // 将比key大的元素向后移动 while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; // 元素后移 j--; } arr[j + 1] = key; // 把key插入到正确的位置 } }快速排序快速排序使用了“分治”的思想，每次选择一个元素，以这个元素为基准，将数组分为小于等于基准和大于基准的部分。再递归地对两边进行排序（也就是对左右两个区间再此选择基准进行划分，如此重复）。快速排序的步骤：选择基准：从数组中选择一个元素作为基准。基准元素的选择可以是随意的。分区：重新排列数组，使得所有小于基准的元素放在基准前面，大于基准的放在后面。分区结束后，基准处于数组的中间位置。递归：递归地将小于基准的子数组和大于基准的子数组排序。public void quickSort(int[] array) { if (array == null || array.length <= 1) return; sort(array, 0, array.length - 1); } private static void sort(int[] array, int low, int high) { if (low >= high) return; int pivot = array[high]; // 选择最后一个元素作为基准 int i = low - 1; // 较小元素的边界指针 // 分区操作：将小于基准的元素移到左侧 for (int j = low; j < high; j++) { if (array[j] <= pivot) { i++; swap(array, i, j); } } swap(array, i + 1, high); int pivotIndex = i + 1; // 递归排序两边子数组 sort(array, low, pivotIndex - 1); sort(array, pivotIndex + 1, high); } // 交换两个元素的方法 private static void swap(int[] array, int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; }归并排序归并排序也用到了“分治”的思想，先将序列分为若干个有序的小区间，最后将这些区间逐步合并。归并排序的步骤：分解：将数组从中间分成两部分。解决：递归地对左右两部分进行归并排序。合并：将两个有序的子数组合并成一个有序数组。 public void mergeSort(int[] array) { if (array == null || array.length <= 1) return; int n = array.length; int[] temp = new int[n]; for (int size = 1; size < n; size *= 2) { // 内层循环处理每个子数组 for (int left = 0; left < n; left += 2 * size) { int mid = Math.min(left + size - 1, n - 1); int right = Math.min(left + 2 * size - 1, n - 1); // 合并操作 int i = left; int j = mid + 1; int k = left; // 比较并合并两个子数组 while (i <= mid && j <= right) { if (array[i] <= array[j]) { temp[k++] = array[i++]; } else { temp[k++] = array[j++]; } } // 处理剩余元素 while (i <= mid) temp[k++] = array[i++]; while (j <= right) temp[k++] = array[j++]; } } }堆排序堆排序的步骤：构建初始堆（这里以升序排序为例，构建大顶堆）。将堆顶元素与末尾元素交换，将最大元素"沉"到数组末端。重新调整堆结构（使其满足大顶堆性质），然后继续交换堆顶元素与当前末尾元素。反复执行调整+交换步骤，直到整个序列有序。public static void sort(int[] arr) { if (arr == null || arr.length <= 1) return; // 1. 构建大顶堆（从最后一个非叶子节点开始） for (int i = arr.length / 2 - 1; i >= 0; i--) { // 堆调整逻辑（原heapify方法内容） for (int root = i; root < arr.length; ) { int largest = root; int left = 2 * root + 1; int right = 2 * root + 2; // 找出根节点和子节点中的最大值 if (left < arr.length && arr[left] > arr[largest]) largest = left; if (right < arr.length && arr[right] > arr[largest]) largest = right; // 如果最大值不是当前根节点，交换并继续调整 if (largest != root) { int temp = arr[root]; arr[root] = arr[largest]; arr[largest] = temp; root = largest; // 移动到交换后的位置继续调整 } else { break; // 当前子树已满足堆性质 } } } // 2. 排序：逐个提取最大值 for (int size = arr.length - 1; size > 0; size--) { // 将堆顶元素（最大值）与当前末尾元素交换 int temp = arr[0]; arr[0] = arr[size]; arr[size] = temp; // 调整剩余元素 for (int root = 0; root < size; ) { int largest = root; int left = 2 * root + 1; int right = 2 * root + 2; // 找出根节点和子节点中的最大值 if (left < size && arr[left] > arr[largest]) largest = left; if (right < size && arr[right] > arr[largest]) largest = right; // 如果最大值不是当前根节点，交换并继续调整 if (largest != root) { int swapTemp = arr[root]; arr[root] = arr[largest]; arr[largest] = swapTemp; root = largest; // 移动到交换后的位置继续调整 } else { break; } } }桶排序这个算法在写题目的时候遇到了，我之前对这个算法不太熟悉，所以重新学一下。桶排序的思想是，找出序列中的最大值和最小值，然后分配若干个桶（桶的数量约等于序列中的元素个数），将每个元素放置到对应的桶中，对每一个桶中的元素进行排序，最后把所有桶中的元素整合成排序后的序列。步骤如下找到数组中的最大值和最小值，以确定数据的范围。确定桶的数量，每个桶代表一个区间范围。遍历数组，将每个元素分配到对应的桶中。对每个桶内的元素进行排序（使用Java内置的排序即可）。按顺序将各个桶中的元素合并回原数组。第三步中，确定桶的位置，我的理解是“相对”位置，即当前元素相对于序列中最小元素“后移”（可能我描述的不是很恰当，但我觉得看代码就能理解了）。 public static void bucketSort(int[] arr) { if (arr.length == 0) return; // 1. 找出数组中的最大值和最小值 int min = arr[0]; int max = arr[0]; for (int num : arr) { if (num < min) min = num; if (num > max) max = num; } // 2. 计算桶的数量和范围 int bucketCount = arr.length; // 通常桶的数量等于元素数量 int range = max - min + 1; // 3. 初始化桶 ArrayList<ArrayList<Integer>> buckets = new ArrayList<>(bucketCount); for (int i = 0; i < bucketCount; i++) { buckets.add(new ArrayList<>()); } // 4. 将元素分配到各个桶中 for (int num : arr) { // 计算元素应该放入哪个桶 int bucketIndex = (int) ((long) (num - min) * bucketCount / range); // 处理最大值的情况，确保它落在最后一个桶中 if (bucketIndex == bucketCount) bucketIndex--; buckets.get(bucketIndex).add(num); } // 5. 对每个桶内部进行排序 int index = 0; for (ArrayList<Integer> bucket : buckets) { Collections.sort(bucket); for (int num : bucket) { arr[index++] = num; } } }
我在上半年结束了一段实习。这一段实习说长也不长，说短也不短，我对这一段经历也有很多复杂的情感吧😐，所以就复盘一下，可能会有一些我自己总结的经验。起因起因当然是我想给自己的简历加上一段比较好看的实习经历！现在找工作也越来越卷，加上各种网络平台上有各种传播焦虑的帖子，于是，我决定找一份实习工作。前期准备简历首先需要有一份清晰的简历，简历不需要花里胡哨，只需要呈现出最有效的信息(面试官想要看到的信息)，包括但不限于学历信息，求职意向，各种奖项和证书信息。针对每个不同行业，会有一些细化的要求，比如我找的计算机行业的开发工作就要写自己的技术栈。简历的模板有很多，在 各种APP上搜索“求职的方向+简历模板”就能搜到很多示例模板，我使用的是overleaf上的一个免费模板，根据小红书上的攻略自己改的简历，目前来看还有点粗糙。如图，进入overleaf后点击图片上图标上面的Templates（模板）跳转到模板界面搜索简历模板，关键词建议使用“简历/resume”。搜索出的模板都会有提示信息告诉你怎么修改，所以不懂latex语法也不用担心~专业知识专业知识多少要了解一下，面试的时候可能会问相关的名词术语（特别是之前没有实习经历的情况下）。我找的职位是Java后端开发，所以面试之前我去看了看所谓的”八股文“（虽然后面证明我看的这些都没什么用😇）。面试/笔试前期的一些准备工作做完之后，就可以开始寻求实习机会了！我在手机上下载了“海量”求职APP，同时在这么多APP上和一些HR进行沟通。建议在第一次和HR发消息的时候就简要说明自己的求职意向，学历背景，可实习时间等，提高效率。我大概和十几个HR都有沟通，最后是一家公司给我发了笔试的题目，另外一家公司约了线下的面试。这里补充几点判断一个公司是否靠谱的方式：招聘APP里面基本都有公司注册资本，公司人数少于50人，注册资本少于100万的谨慎考虑公司成立时间最好在20年之前，这说明这家公司有一定的抗风险能力招聘岗位信息含糊（没说清楚岗位职责），但是标注的工资却很高的岗位谨慎考虑HR每日和人交流次数很多的，有可能是HR在完成指标，找人意愿可能并不是很强在网络上有各种平台可以查到公司每年缴纳社保的人数，人数很少说明这家公司可能只会给一部分人缴纳社保（这可能同时意味着公司对员工压榨严重）然后就是做笔试题目，然后去线下面试了。笔试的题目对我来说太难了，这里就不说了。线下面试的一些题目倒是可以复盘一下：Java的特性四种类修饰符号的区别如果设计一个系统，有管理员、高级领导、普通用户这三种身份，要建几张表ArrayList与LinkedList的区别常用的linux命令Java创建对象的方式线程中实现同步的关键字wait()和sleep()的区别其中5~8是线下笔试的相关知识点，前面的是面试提的问题。现在想来，我当时的表现可谓是一塌糊涂，能找到实习完全是凭借运气🤨。实习感受面试通过之后一星期我就正式开始实习工作了。因为是实习生，我接触到的基本都是比较成熟的项目。这些项目基本都是使用Vue+Spring boot或Vue+Spring cloud的框架，项目优化过程中要看大量的前端代码，我也不得不去学一些前端的知识。在修复bug的过程中，我觉得最困难的并不是“找到”解决方案，而是理解业务逻辑，然后一边改代码一边和各种各样的小问题斗争😇，同时要考虑到数据库性能，服务器性能。虽然我的工作量不大，但我也算是见过一些奇葩的bug，见过一些稀烂的代码了（变秃了也变强了）。每天做了什么我都会写成一个简单的日记，然后每一周写一次周记，把自己学到的一些处理问题的方法记录一下，方便之后复盘。这是我用obsidian整理的日记和周记网络最后第一次实习，我认识到自己还有很多要学习的，感受到了打工人的疾苦。也意识到有时候自己也不够主动，不会的问题不太愿意主动去问带教，这在职场中可能不太好。总之，我还是觉得这段经历让我成长了很多，即使在这过程中我崩溃了十几次😫。
时间过得真快光阴似箭，岁月如梭，一转眼就到了我这个小网站建立一周年的时候了，让我先庆祝一下吧🥳🎉！一年时间里，我在网站上发布了36篇文章，并且给这些文章分好了类。其中“编程”分类下的文章数量最多，浏览量也最大（可能爬虫就喜欢这种关键词的文章🤔）。网站的外观也从一开始较为简单的主题换成了现在这个功能齐全的主题，在折腾网站的路上踩了不少坑，也学到了很多，包括但不限于网站备案，域名绑定，图床设置。虽然这些知识看起来似乎实用性不强，我还是乐在其中，在现在这个时代有一件自己痴迷的事情是一件很珍贵的事。然后感谢我的两位朋友Chailyn's homepage和路红叶先生/YZ | HongYe’s Blogspace，在他们的帮助下我不断完善自己的网站，并坚持写作。他们的网站可以在友链界面点击后跳转。希望这个网站还能坚持运营两年三年甚至更长时间，我会尽力维持现在的更新频率，感谢每一位曾经到访过这个小小网站的朋友🙏。
又到了盘点我读了哪些书的时候了😄。上半年有点忙（虽然也不知道在忙些什么），读书的时间短了很多，看的书自然比去年同期少了一些。上半年看的书以小说为主，加上一点点科普类的书籍，我的文学水平不高，自然也没有能力阅读一些著作。2025.1长夜难明这是今年看的第一本书，我是在豆瓣的榜单上看见这本书的评分很高，于是就想读读试试。读完觉得有些失望，小说的文笔感觉很一般，情节感觉也没有很真实。我用中文做了场梦这本书的亮点就是外国人用中文写的，在中国的见闻，有趣的书。2025.2告白最后的反转出乎意料，第一人称的视角来叙述整个故事在推理小说里面还是很少见的。金色梦乡很特别的一本书，说是推理小说好像又不太像，但是情节的设置又很像推理小说。我觉得这本书和《长夜难明》想要表达的中心思想是相似的，不过中日作家的表现方式有所不同，我更喜欢这一本的语言还有情节。13 67准确来说是去年读的书了，但是豆瓣有关这本书的词条老是炸掉😢，今年才标记上。里面有一些反映时代背景的描述，整本书的各个章节之间很连贯，伏笔也很巧妙。2025.3我们为什么要睡觉？看完这本书以后我决定好好睡觉（开个玩笑😊）！说实在的这本书读起来有点乏味，因为里面使用的一些专业术语让人有一种陌生感，加上叙述也是平铺直叙，就是一本普通的科普图书。2025.4龙文身的女孩又是一本北欧的小说，之前也看过一两本北欧作家写的悬疑小说。这些作家的作品总是有一种非常“冷”的恐怖氛围，我猜想或许和北欧的高纬度有关。书中关注了女性受到暴力这一话题，但总觉得写得不够深入。或许是地域文化的差距，北欧作家的文字不像东亚作家那样细腻。2025.5迷人的材料介绍了各种常见的材料，看完并没有喜欢上材料科学😁，或许应该推荐给材料专业的朋友看看。一本通俗易懂的科普书，或许更适合中小学生读🤔。2025.6钟表馆事件本格推理？我不懂各种推理流派的划分。这本书的风格有种“年代感”，作案手法也比较原始而巧妙。我也看了不少推理小说，但始终没能找到符合我的心理预期的“完美”的推理小说。总结一下，上半年看了九本书，基本都是小说。所有书都是看一遍就结束，没有让我印象深刻的书。在这个获取的知识都是高度碎片化的时代，我希望自己依然能有耐心去读完一本几百页的课外书。
这个网站从建立到现在以及快一年了，我也在上面写了三十多篇文章。从最开始直接使用云服务器的磁盘存储博客中的图片，到后面逐渐摸索出用Typora+Picgo+对象存储实现写作的同时便捷地上传博客文章中的图片。下载相关软件typora下载官网指路->Typora 官方中文站typora有一个缺点就是，它现在收费了。PicGo下载官网指路->PicGo购买对象存储因为要把图片传到对象存储中，所以先要购买相关的服务。至于选择买哪一家的服务就全凭个人喜好了，建议选择大厂商的服务防止跑路，一般来说各大厂商的价格之间差别都不是很大，我目前使用的是七牛云的服务。如果是第一次使用对象存储需要对域名进行一些设置，这里就不赘述，详细的教程可以在网上搜索。相关配置首先在PicGo里面进行图床的配置，在PicGo的页面选择你使用的对象存储进行设置，设置里面的Bucket和Key等需要在对象存储厂商设置中获取到，不同厂商的配置略有不同，不太清楚整个流程的话可以网上搜索对应厂商的PicGo配置教程。在typora中配置PicGo的路径打开typora依次点击左上角的“文件”->偏好设置->图像，进入以下界面在右下角上传服务设置里面选中PicGo，并且根据安装的位置设置好PicGo的路径，进行上述设置之后可以点击验证图片上传选项按钮进行验证，验证成功说明上传图片的设置已经设置好了。写作经过以上一通设置之后就可以开始愉快地写博客了！写作的时候可以直接把要插入的图片复制粘贴进来，然后就可以看见图片的本地链接。之后把光标移动到这个链接上面，鼠标右键点击上传图片，图片就会自动上传到配置的对象存储里面。自动上传后会发现链接变成了图床里面的图片链接，但是图片有可能不显示，这时候检查一下图床的图片链接，有可能多了几个字符，比如我在上传图片后，图片的链接会多出“%20”这几个字符，每次上传完毕后手动删除掉这几个字符，图片就能在typora里面显示出来了。例：上传后链接会变为http://%20image.xxx，删除“//”之后的“%20”即可
今天是六月七号，高考的第一天，看着微博热搜榜上被各种有关“高考”的词条占领，我也想回忆一下我的高考，蹭一下高考的热度😝。前一天去看了自己的考场，还意外地看见了一个初三的同班同学。中考后班上选择到市区学校上学的不超过五人，能在此时又见到同班同学，有种说不出的兴奋。六月六号那天晚上还出去吃晚饭了，因为实在受不了食堂了🙃。考前周围的人都挺轻松的，我内心却是紧张与焦虑，两天晚上都是一点多才睡着💤。早上因为要坐公交车去考场，必须起得比较早。那几天的天气并不是很好，有点下雨，而且有些闷。密密的雨总给人淡淡的忧伤之感。关于考试的题目几乎记不清楚了，只剩下大概的印象。第一天是语文和数学，语文的作文题目是《跨越，再跨越》，而且有些题型变化了，没有前两年的题目好做。数学考得偏难，很多个大题我都没有写完，选择题也没有很大把握，从考场出来的时候还看见有人哭了😢，不过我还是尽力使自己保持镇定，因为第二天的理综对我来说也不容易。第一天晚上还有老师来宿舍安慰我们：难度偏大对所有人来说都是一样的，好好复习明天的考试。理综中规中矩，考的时候不觉得很难（相对于数学来说），只是从最后的分数来看有点惨淡😂，英语是最简单最轻松的一门，考完英语就意味着彻底结束了。所有考试都结束后，我坐在公交车上有一种解脱感，这几年的“折磨”终于结束了。有人和老师讨论起了理综的选择题，有人计划着去玩。那一刻，也许大家都真正体会到班主任所说的“世界都在你的脚下”的感觉吧。感叹一句，伴随着高考结束，我的中学生活也这样结束了。那几天就这样过去了，当时看起来特殊的日子，现在再看好像也不过如此。还是很羡慕当时考完解脱的感觉，也许我不会再有类似的感受了，😮‍💨。