0%

github Pages + Hexo 博客搭建

前置知识:git 使用,github 建库

注意GitHub仓库最好建的是username.github.io仓库,这样GitHub Pages 才能获得一级域名。而二级域名在进行某些扩展时有问题。可以去搜一下关于github pages两种域名的区别。

安装环境

安装 node.JS

官网下载安装程序,默认安装即可

命令行运行node -v npm -v检查安装效果

安装 hexo

注意以下命令推荐使用字符支持良好的命令行,最好给管理员权限。我使用的是 git bash。

可以使用 cmd,但我遇到了中文问题。

powershell 运行会提示 “在此系统上禁止运行脚本”, 管理员权限下输入 set-executionpolicy remotesigned 更改安全策略,允许运行脚本。

运行npm install -g hexo-cli进行安装,再运行hexo -v检查安装效果,有版本信息即安装成功

配置项目

初始化

安装成功后自己创建一个项目文件夹,命令行切换到文件夹路径进行以下初始化:

运行hexo init初始化

本地测试

运行以下命令部署默认网页进行测试:

1
2
3
4
5
hexo new test_my_site #创建新 test_my_site 的 markdown 文件

hexo g # 从 md 文件生成 html 文件

hexo s # 部署本地服务器

观察到命令行在持续运行时,则可打开浏览器输入本地服务器地址(默认是localhost:4000)访问试试看。

部署到网络

在文件夹根路径下面,找到_config.yml,打开修改

最后一段为:

1
2
3
4
deploy:
type: git
repo: 仓库的完整路径,推荐使用 ssh 路径,这样可以有 ssh 免密登录。例如我的 git@github.com:Alobal/alobal.github.io.git
branch: master

URL 小段为:

1
2
3
4
# URL
## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/'
url: 访问网址,例如 https://alobal.github.io/
root: 仓库名,例如 /

保存关闭该文件

安装 hexo 的 git 部署插件npm install hexo-deployer-git --save

运行以下命令进行发布到网络

1
2
3
hexo clean
hexo g
hexo d

在 hexo d 部署到 GitHub 时可能要求 GitHub 账号密码。

出现Branch 'master' set up to track remote branch 'master' from 'https:..大概就恭喜你成功了。此时可以在 github 仓库看到 hexo 提交的文件。以及能通过访问网址查看你刚才发布的默认博客。

选一个你喜欢的主题

下载主题

hexo 主题库

挑选一个合适的主题,比如很常见的 Next,复制 git 地址,在本地博客项目根目录下,命令行输入进行 git clone

例如git clone https://github.com/next-theme/hexo-theme-next themes/next

如果使用 next 主题请注意, next 主题在 2020 之前是 theme-next 项目,2020 版本是 next-theme 项目,配置字段有点不同,根据需要选择。19 版本网上教程比较健全了。

配置主题

打开 _config.yml,修改 theme 字段:

1
2
3
4
# Extensions
## Plugins: https://hexo.io/plugins/
## Themes: https://hexo.io/themes/
theme: 主题名,例如 next

其中主题也有自己的配置文件,项目名/themes/主题名/_config.yml,可以根据需要进行修改。比如 next 主题配置文件中,可以在四种风格中选一种,选一个取消注释就好了。想预览效果可以去找 next 官方文档。

1
2
3
4
5
# Schemes
#scheme: Muse
#scheme: Mist
scheme: Pisces
#scheme: Gemini

输入hexo g;hexo d 重新生成网页并发布出去,可以自己偷偷欣赏欣赏。

如果没看到生效,可能是因为浏览器缓存,关掉浏览器重新打开,或者 Ctrl+F5 强制刷新。

hello,blog

让我们试着发布第一篇博客。项目路径下,命令行输入hexo n 博客名 , 可以在 项目名/source/_posts/ 下面找到新建的 markdown 文件。打开文件可以发现有预置的文件头,暂时不用管,后续要添加分类标签功能的时候可以写一写。在文件头之后,即------下面写入你的 markdown 格式内容即可。

关于 markdown 怎么书写,可以看我的 markdown 简洁手册

参考

知乎经典贴

错误汇总

can not find module xxx

命令行npm install 安装所有依赖包完事。

没有启用代码高亮

根目录 config 里,修改配置如下:

1
2
3
4
5
6
7
highlight:
enable: true
line_number: true
auto_detect: true
tab_replace: ''
wrap: true
hljs: true

next 主题配置里,选择自己要的高亮样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
codeblock:
# Code Highlight theme
# All available themes: https://theme-next.js.org/highlight/
theme:
light: tomorrow-night-blue
dark: tomorrow-night-blue
prism:
light: tomorrow-night-blue
dark: tomorrow-night-blue
# Add copy button on codeblock
copy_button:
enable: true
# Available values: default | flat | mac
style:

众所周知,派生类构造时先会初始化构造基类,然后构造自身。

派生类私有成员 和 基类 同时初始化

假如派生类的构造函数,初始化时显式初始化基类,同时显式初始化派生类成员,那么先后顺序是怎样的呢? 测试背景:

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
#include<iostream>
using namespace std;
class base
{
int x;
public:
base(int i) :x(i) { }

void dispb()
{
cout << "x=" << x << " " << "base
";
}
};

class derived : public base
{
int a=0;
public:
derived(int i) :a(i), base(a) { }

void dispd()
{
cout << "a=" << a << " " << "derived
";
}
};
int main()
{
derived obj(8);
base*p =&obj;
p->dispb();
system("PAUSE");
return 0;
}

如上,派生类Derived构造时,基类初始化base(a),用了派生类私有成员a,同时a也被参数i初始化。此时因为基类初始化在a初始化之前,传给base()的a是随机值。输出结果是:

x!=i

所以我们可以知道,即使是初始化列表里,即使a的初始化排在前面,也一定是先初始化基类base,再调用派生类私有成员的初始化。

想要正常构造,把base(a)改成base(i)即可。

派生类初始化私有成员,基类在函数体内构造

假如派生类的构造函数是这样的:初始化私有成员,体内构造基类

1
derived(int i) :a(i) { base(i); }

会提示类base不存在默认构造函数。

我们可以得知,在派生类构造函数中,假如初始化了自己的变量成员,则会自动先调用基类构造,再对派生类的成员进行初始化,上例因为base没有无参数的默认构造函数所以报错。

假如基类有无参数的构造函数呢?讲道理会是先调用基类默认构造,再初始化a,再调用base(i),是这样吗?增加一个无参数的基类构造函数:

1
2
3
base() { cout <<"This is base()"<< x << endl; }
.....
derived(int i) :a(i) { base(i); }

报错:形参 i 重定义——明明只声明了一次 i ,怎么就重定义了呢?把a(i)删去?还是报错重定义

——搜了一下类似问题,发现题解,对base(i)在里面的时候:“此时编译器会把它解析成变量声明,由此局部变量i与函数参数重名。根据语法,它可以被解释成函数式显式类型转换或者声明,存在二义性。标准约定将其解释成声明。”

这里面存在 声明 语句 定义 等根本性语法的规则定义= =具体了解得看其他详细资料,不过我们知道了报错是因为在 base(i) 中,i被当成了新的局部变量,和外面的函数参数重定义冲突。

i 冲突那我不用 i 了,我用base(a)可以吧?a是被初始化的变量拿去赋值没问题吧?

派生类构造函数改成:

1
2
3
4
base(int i) :x(i) { cout <<"base(int i): "<< x << endl; }
base() { cout <<"This is base()"<< x << endl; }
......
derived(int i):a(i){ cout << "a:" << a << endl; base(a); }

derived(8)运行,猜猜运行结果是什么?

我们知道初始化a时,先会调用base(),然后初始化a,然后进入函数体内输出a值,然后a传参给base(int i),Really?

运行结果

两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。 你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

示例: 给定 nums = [2, 7, 11, 15], target = 9 因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]

1.解决思路

暴力遍历,我们需要找出两个数满足一个数值和的需求,我首先想到的就是通过遍历尝试,简单暴力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
vector<int twoSum(vector<int& nums, int target)
{
vector<int result;
int i=0;
int j=0;
for(i=0;i<nums.size()-1;i++)
{
for(j=i+1;j<nums.size();j++)
if(nums[i]+nums[j]==target)
{
result.push_back(i);
result.push_back(j);
j=nums.size();
i=j-1;
}
}

return result;
}
};
  • 通过
  • 472 ms
  • 9.2 MB
  • Cpp

2.优化思路

显然暴力解法最容易想但效率也大量浪费在了无谓的二层循环中,直接导致了O(n^2)的复杂度。 细化需求,当我们找到一个数,本来是要在二层循环中尝试找到可以组合的数,我们已知数值寻找位置,因此可以考虑到使用map哈希表来构造数据结构,使用 空间换时间。 哈希表查找复杂度为O(1)

两遍哈希表,我们把数组遍历存入map中,消耗O(n),然后再对每一个数进行遍历,每次只要在map中寻找其对应的数值是否存在即可,总时间复杂度为2O(n)。 一遍哈希表 :在存入map时即对对应的匹配数进行查找,优化复杂度至O(n)。

3.最终实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<int twoSum(vector<int& nums, int target)
{
map<int,int store;
vector<int result;
for(int i=0;i<nums.size();i++)
{

if(store.count(target-nums[i])0)
{

result=vector<int({store[target-nums[i]],i});
break;
}
store.insert(map<int,int::value_type(nums[i],i));
}

return result;
}
};
  • 通过
  • 20 ms
  • 10.1 MB
  • Cpp

正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。

'.' 匹配任意单个字符 '*' 匹配零个或多个前面的那一个元素 所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

说明:

s 可能为空,且只包含从 a-z 的小写字母。 p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。 示例 1:

输入: s = "aa" p = "a" 输出: false 解释: "a" 无法匹配 "aa" 整个字符串。

示例 2:

输入: s = "aa" p = "a" 输出: true 解释: 因为 '' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3:

输入: s = "ab" p = "." 输出: true 解释: "." 表示可匹配零个或多个('*')任意字符('.')。

示例 4:

输入: s = "aab" p = "cab" 输出: true 解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。

示例 5:

输入: s = "mississippi" p = "misisp*." 输出: false

解题分析

条件特性

  • 首先可知"." 和对应字符的匹配都很容易处理,关键要处理好这个" * ".
  • " * "带来的变化有, 使前一个字符匹配N次,N可为0,也就是说也可以忽略前一个字符. #### 错误思路 ~~由于我一开始没有注意到N可以为0,就采用了一个逐字符匹配的方法,假如遇到一个字符后面有"",如果匹配错误则直接返回,匹配正确则维持pi不变,这样一步步推过去,在最后若两个字符串都到了结尾,则成功.后来提交错误后有针对性的修修补补浪费了很长时间,并且发现这种解法下再怎么改也不能AC,最终还是推盘重做T-T~~ ## 解题思路 每一次带""的字符处,都会有两种可能的情况,要么匹配这个字符,要么不匹配这个字符. 我们在测试一种情况发现不行之后,需要再退回测试另一种情况下的匹配.因此可以想到可以使用递归返回.

递归体设置

1.若pi+1处为" * ",则分情况处理 1.1. 如果pi处可正确匹配,则分两个递归,一个是pi匹配1次,一个是pi匹配0次. 1.2. 如果pi处不能匹配,则以pi匹配0次向后递归处理 1.3. 考虑特殊情况,s已经被带" * "字符匹配完,s为空,p还有至少两个字符存在,则p向后推进两个字符再和已经空的字符进行递归匹配.

2.若pi处为"."或可匹配字符,则正常匹配成功,s和p都往后推进1个字符.

3.若pi处为无法匹配字符,return false ### 递归出口 因为我们的递归体若能成功匹配,则S在最底层的递归中,必定是一个空串 - 此时若p为空串,则s和p恰好完全匹配成功,return true - 若p不为空,则s肯定是被带" * "字符消化掉了,因此参考上面的递归体条件,进入下一次递归中p的带""字符被删掉,此时再进行出口判断.如果p仍不为空,即可return false. 注意p不为空时,有两种情况,并且后一种情况是前一种情况的递归结果 因此不能简单的直接根据plength!=0直接返回false. 要先判断p是否有字符,若有字符并且是带" "字符,则是可匹配范围内的情况,需递归推两个字符进一步判断. 若p有字符并且不是带"*"字符,则是无法正确匹配的情况,return false.

初版通过代码

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
class Solution {
public:
bool isMatch(string s, string p)
{
int si = 0;
int pi = 0;
int slength = s.length();
int plength = p.length();
if (slength != 0 && plength == 0)
return false;

if (slength == 0 && plength == 0)
return true;
else
{

if (p[pi + 1] == '*')
{
if (slength == 0)
return isMatch(s.substr(0, 0), p.substr(pi + 2, plength - pi - 2));
else if(p[pi] == '.' || p[pi] == s[si])
//pi匹配1次s前进p不变 pi匹配0次s不变p前进2
return isMatch(s.substr(si + 1, slength - si - 1), p.substr(pi, plength - pi)) || isMatch(s.substr(si, slength - si), p.substr(pi + 2, plength - pi - 2));
else
return isMatch(s.substr(si, slength - si), p.substr(pi + 2, plength - pi - 2));
}

else if (slength == 0)
return false;
else if (p[pi] == '.' || p[pi] == s[si])
return isMatch(s.substr(si + 1, slength - si - 1), p.substr(pi + 1, plength - pi - 1));
else
return false;
}

}
};
  • 624 ms
  • 16.4 MB
  • Cpp 虽然已经AC了,时间空间上还是很笨拙的,日后找时间探索优化解法. ------------- 动态规划,以dp[i][j]为状态标志位,为1表示s的前i个字符和p的前j个字符可以匹配。 注意dp[0][0]==1表示空字符串时可以匹配。
    1
    2
    3
    //两层for循环
    for(i)
    for(j)
    通过判断当前字符匹配情况,加上前面的子串情况进行累积动态规划,最终达到dp[slength][plength]的值。

理解了思路,但实战这道题还是有细节方面思路不清,继续挖坑。

盛最多水的容器

给定 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

说明:你不能倾斜容器,且 n 的值至少为 2。

图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

 image.png

示例:

输入: [1,8,6,2,5,4,8,3,7] 输出: 49

解题分析

  • 双循环暴力法是人类本能,就不侃了,直接放上辣眼睛的效率结果
通过情况 时间 内存 语言
通过 2436 ms 9.8 MB Cpp
  • 正解: 双指针向内收缩法.即一指针在头部,一指针在尾部, 而收缩时面积由两端之短决定,我们要找到更大的面积,则必定要改变两端之短,所以每次将短的那边往里收缩,遍历O(n).
  • 正确性: S=min(ai,aj) (j-i),假如大的那一边往里收缩则S=min(bi,bj) (j-i-1). 则min(bi,bj)要么是比收缩前最小值还小的值,要么是收缩前的最小值,所以S必定变小. 因此双指针移动大的一端时,得到的S2<=S1,肯定不是正确方向,所以可以省略掉这一系列的遍历,即只移动每对中小的那一端即可,省下O(n)的遍历不需要尝试,最终的遍历效率是O(n).

解题代码

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
class Solution {
public:
int maxArea(vector<int& height)
{ //双指针 思考正确性
int i=0;
int j=height.size()-1;
int Area;
int MaxArea=0;

while(i<j)
{
int minvalue=min(height[i],height[j]);
Area=minvalue*(j-i);
if(AreaMaxArea)
MaxArea=Area;

if(height[i]==minvalue)
i++;
else
j--;
}

return MaxArea;
}
};