Hexo 博客配置
环境准备
本地安装 Git NodeJS
检查环境
1 | git -v |
切换镜像站,具体参考NPM配置国内源
1 | npm config set registry https://registry.npmmirror.com |
Hexo环境搭建
1 | pnpm install -g hexo-cli # 安装Hexo cli工具 |
Hexo 配置
参考官方文档
1 | ... |
注意: 评论部分需要借助Github Discussions, 参考Hexo博客配置Giscus评论
Hexo主题配置
安装主题后从npm_modules/<主题名>/
文件夹中复制_config.yml
到博客根目录并重命名为_config.next.yml
,当博客deploy时回自动应用主题配置,一下主题修改都基于此文件进行。
设置语言
NexT主题支持多种语言,只需要编辑_config.next.yml
中的language
设置即可
语言 | 代码 | 设定示例 |
---|---|---|
English | en | language: en |
简体中文 | zh-CN(注:zh-Hans已经无法使用) | language: zh-CN |
Frangais | fr-FR | language: fr-FR |
Portugues | pt | language: pt 或者 language:pt-BR |
繁體中文 | zh-hk 或者 zh-tw |
language: zh-hk |
Pycckmi 93bIK | ru | language: ru |
Deutsch | de | language: de |
日本語 | ja | language: ja |
Indonesian | id | language: id |
Korean | ko | language: ko |
如果需要添加非内置的字段需要手动添加翻译文件,例如中文的翻译文件路径为node_modules/next/languages/zh-CN.yml |
设置关于
在source/about/index.md
中添加如下内容
1 | --- |
选择Scheme
Scheme 是 NexT 提供的一种特性,借助于 Scheme,NexT 为你提供多种不同的外观。同时,几乎所有的配置都可以 在 Scheme 之间共用。目前 NexT 支持三种 Schem
- Muse - 默认 Scheme
- Mist - Muse 的紧凑版本
- Pisces - 双栏 Scheme
- Gemini
菜单配置
菜单配置包括三个部分,第一是菜单项(名称和链接),第二是菜单项的显示文本,第三是菜单项对应的图标。 NexT 使用的是 Font Awesome 提供的图标, Font Awesome 提供了 600+ 的图标,可以满足绝大的多数的场景,同时无须担心在 Retina 屏幕下 图标模糊的问题。
1 | menu: home: / || home |
NexT 默认的菜单项有(标注 *
的项表示需要手动创建这个页面):
注意: 若站点运行在子目录中,请将链接前缀的 / 去掉。
键值 | 设定值 | 显示文本(简体中文) |
---|---|---|
home | home: / | 主页 |
archives | archives: /archives | 归档页 |
categories | categories: /categories | 分类页 * |
tags | tags: /tags | 标签页 * |
about | about: /about | 关于页面* |
commonweal | commonweal: /404.html | 公益 404 ! |
侧栏配置
默认情况下,侧栏仅在文章页面(拥有目录列表)时才显示,并放置于右侧位置。配置具体如下
1 | ... |
侧栏显示位置支持
left
: 居左显示right
: 居右显示
侧栏显示行为支持
post
默认行为,在文章页面(拥有目录列表)时显示always
所有页面都显示hide
在所有页面中都隐藏(可以手动展开)remove
完全移除
注册Github账号,Gitea账号(可选)
[^注] Github由于网络问题会经常无法链接,可使用Gitea作为中转,先将代码提交道Gitea,然后Gitea配置自动推送到Github
设置头像
1 | avatar: /images/avatar.jpg |
头像地址如果是以/
起始则表示头像图片放置在博客发布后的目录下,例如测试博客地址是http://localhost:4000
,头像图片地址为http://localhost:4000/images/avatar.jpg
此配置需要在博客的source/images
目录中放置头像图片avatar.jpg
侧边栏社交链接
1 | social: |
next主题默认支持的社交链接 ||
符号后是链接的图标
使用已有配置放开注释即可,如果要添加默认不存在链接示例如下
1 | social: |
注意: 图标对应的名称是FontAwesom图标的名称(不必带 fa-
前缀)
打赏功能
1 | # Reward |
放开此部分注释并在source/images
中放入收款码图片
站点建立时间
1 | footer: |
订阅微信公众号
1 | # Wechat Subscriber |
放开此部分注释,并在source/images
中放入公众号二维码
注意: 此功能需要NexT版本在5.0.1之后
设置动画
NexT 默认开启动画效果,效果使用 JavaScript 编写,因此需要等待 JavaScript 脚本完全加载完毕后才会显示内容。 如果您比较在乎速度,可以将设置此字段的值为 false
来关闭动画。
1 | # Use velocity to animate everything. |
设置全文阅读
在首页显示一篇文章的部分内容,并提供一个链接跳转到全文页面是一个常见的需求。 NexT 提供三种方式来控制文章在首页的显示方式。
- 在文章中使用
<!-- more -->
手动进行截断,Hexo 提供的方式 推荐。 - 在文章的 front-matter 中添加 description,并提供文章摘录
- 自动形成摘要,需要添加如下配置
1
2
3
4
5# Automatically Excerpt. Not recommend.
# Please use <!-- more --> in the post to control excerpt accurately.
auto_excerpt:
enable: true
length: 150
设置字数统计/阅读时长
在_config.yml
中配置如下
1 | # Post wordcount display settings |
加载进度条
1 | # Progress bar in the top during page loading. |
搜索服务
在_config.yml
中配置如下
1 | # hexo-generator-searchdb |
在_config.next.yml中配置如下
1 | # Local search |
使用Github Actions部署Hexo博客
前言
作为技术博主,博客的高效维护与部署一直是我关注的重点。近期在维护博客时,我遇到了两个核心问题:
- 内容管理混乱:草稿箱文件堆积,缺乏分类标准,甚至因误操作破坏了原有配置;
- 兼容性局限:计划将文章同步至 FastGPT 等 AI 知识库时,发现官方推荐的 Hexo 部署方案(源码与静态文件混存)中,冗余的
public
目录会干扰 RAG 系统提取内容,且源码与发布产物耦合易引发冲突。
为解决这些问题,我采用了源码与发布分离的部署架构:将 Markdown 源文件单独存放在一个仓库,通过 GitHub Actions 自动在另一个仓库构建并发布静态文件。这种方式的优劣对比如下:
方案 | 优点 | 缺点 |
---|---|---|
官方混仓部署 | 支持本地手动 / 自动发布,预览方便,配置简单 | 仓库体积大,源码与产物混合,不利于二次利用 |
本文分离部署 | 源码纯净、产物独立,兼容 AI 知识库,自动构建 | 本地预览需搭测试环境,配置较复杂(双仓库 + 鉴权) |
部署核心思路
核心逻辑:当源码仓库收到推送时,GitHub Actions 自动将源文件检出到 source/_posts
,并从 _hexo
目录复制配置文件还原 Hexo 环境,最终执行构建与发布。
文件结构设计(源码仓库):
1 | |-- _hexo/ # Hexo 核心配置目录 |
详细部署步骤
1. 生成 SSH 密钥对(用于仓库间鉴权)
需要生成一对 SSH 密钥,用于源码仓库向发布仓库推送构建结果:
1 | ssh-keygen -t rsa -C "<github 注册邮箱>" |
执行后会在以下路径生成两个文件:
- 私钥:
~/.ssh/id_rsa
(Linux/Mac)或C:\Users\<用户名>\.ssh\id_rsa
(Windows) - 公钥:
~/.ssh/id_rsa.pub
(同上路径)
注意:.ssh为隐藏目录,需要修改系统设置显示此文件夹

2. 准备两个仓库
仓库 1:源码仓库(存放 Markdown 与配置)
- 新建仓库(例如命名为
hexo-source
) - 进入仓库设置:
Settings → Secrets and variables → Actions → New repository secret
- 添加一个名为
HEXO_DEPLOY_KEY
的密钥,值为私钥id_rsa
的内容(用记事本打开复制)


仓库 2:发布仓库(存放静态文件,用于 GitHub Pages)
- 仓库名必须为
<你的 GitHub 用户名>.github.io
(固定格式,否则 GitHub Pages 无法生效) - 权限需设为公开,并开启
Discussions
功能(进入仓库设置 →Features
勾选) - 配置部署密钥:
Settings → Deploy keys → Add deploy key
- Title 填
HEXO_DEPLOY_PUB
- Key 填入公钥
id_rsa.pub
的内容,并勾选Allow write access
(允许推送权限)
- Title 填


3. 配置 Hexo 环境文件
在源码仓库中创建 _hexo
目录,放入以下核心文件(可从本地 Hexo 环境中复制, 参考Hexo-博客配置):
_config.yml
:Hexo 主配置(需修改部署相关配置,见步骤 4)_config.next.yml
:NexT 主题配置(其他主题同理)package.json
:依赖配置(需包含hexo
、hexo-deployer-git
等核心依赖)scaffolds/
:文章模板(draft.md
/page.md
/post.md
)- 静态资源:如头像(
avatar.jpg
)、关于页(about.md
)等,按实际需求存放
4. 配置部署与工作流文件
① Hexo 部署配置(_hexo/_config.yml
)
在配置文件中添加部署规则,指向发布仓库:
1 | # Deployment |
② GitHub Actions 工作流(.github/workflows/hexo-deploy.yml
)
创建工作流文件,实现自动构建部署:
1 | name: hexo-deploy # 工作流名称 |
验证与使用
- 将上述文件提交到源码仓库的
master
分支,GitHub Actions 会自动触发工作流; - 进入源码仓库的
Actions
标签页,查看工作流执行状态,若显示绿色对勾则部署成功; - 访问
https://<你的用户名>.github.io
,即可看到最新发布的博客。
注意事项
- 私钥
HEXO_DEPLOY_KEY
是敏感信息,切勿泄露或提交到仓库; - 发布仓库名必须严格为
<用户名>.github.io
,否则 GitHub Pages 无法正常访问; - 若主题是自定义修改过的,建议将主题 fork 到自己的仓库,再在工作流中拉取自己的 fork 版本;
- 本地预览时,可在源码仓库中手动搭建 Hexo 环境(复制
_hexo
目录文件,执行hexo server
)。
参考
Kubernetes搭建
节点名称 | 节点IP | 配置 | 系统版本 |
---|---|---|---|
VIP | 192.168.50.220 | 虚拟IP | |
k8s-master-221 | 192.168.50.221 | 4核 2G | debian 11 |
k8s-master-222 | 192.168.50.222 | 4核 2G | debian 11 |
k8s-master-223 | 192.168.50.223 | 4核 2G | debian 11 |
k8s-node-224 | 192.168.50.224 | 4核 2G | debian 11 |
k8s-node-225 | 192.168.50.225 | 4核 2G | debian 11 |
主机配置
时间同步
1 |
配置 hostname
注意节名称不能重复
1 | hostnamectl --static set-hostname k8s-master-221 |
配置防火墙
1 | service iptables stop |
如果需要打开防火墙,执行以下配置
1 | # master节点执行 |
关闭交换分区
1 | swapoff -a |
若需允许交换分区参考官方文档 交换分区的配置
配置hosts
1 | cat >> /etc/hosts << EOF |
开启 bridge 网桥过滤功能
1 | # 桥接的ipv4流量转到iptables |
配置 IPVS
1 | modprobe br_netfilter |
安装工具
安装 Containerd
1 | # 安装 |
设置cgroupdriver
为systemd
,编辑 /etc/containerd/config.toml
文件,找到 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
部分,添加一行内容:SystemdCgroup = true
1 | sed -i 's/SystemdCgroup \= false/SystemdCgroup \= true/g' /etc/containerd/config.toml |

重启containerd并设置开机启动
1 | systemctl restart containerd |
安装 keadm,kubelete,kubectl
1 | # 添加安装源 |
部署高可用(仅 master 节点)
安装
1 | apt install keepalived haproxy |
修改haproxy配置
/etc/haproxy/haproxy.cfg
1 | global |
配置 keepalived
interface # 网卡名称
mcast_src_ip # 节点ip
virtual_ipaddress # vip地址
k8s-master-221配置文件/etc/keepalived/keepalived.conf
1 | ! Configuration File for keepalived |
k8s-master-222配置文件/etc/keepalived/keepalived.conf
1 | ! Configuration File for keepalived |
k8s-master-222配置文件/etc/keepalived/keepalived.conf
1 | ! Configuration File for keepalived |
健康检查脚本 /etc/keepalived/check_apiserver.sh
1 | #!/bin/bash |
给监测脚本添加执行权限
1 | chmod +x /etc/keepalived/check_apiserver.sh |
启动keepalive和haproxy
1 | systemctl daemon-reload |
测试vip漂移
1 | # 查看ip与vip |
初始化集群
拉取镜像
1 | # 查看需要的镜像文件 |
master 节点初始化
1 | # 导出默认初始化配置 |
master 节点加入集群
1 | # master节点需要生成certificate-key |
worker 节点加入集群
1 | kubeadm join 192.168.50.220:16643 --token {token} \ |
从集群种移除节点
1 | kubectl delete node {node-name} |
配置环境变量,用于访问集群
1 | cat << EOF >> ~/.bashrc |
查看集群节点状态
1 | # 查看节点状态 |
安装网络组件(只在master-221节点操作)
Calico
Flannel
去除 master节点污点
如果你打算让Master节点也参与到平常的Pod调度(生产环境一般不会这样做,以保证master节点的稳定性),那么你需要使用以下命令将Master节点上的 taint(污点标记)解除
1 | kubectl taint nodes --all node-role.kubernetes.io/master- |
最后我们使用以下命令查看当前集群的状态,发现Scheduler和Controller Manager组件处理不健康状态:
1 | kubectl get cs |
解决上述问题需要将每个Master节点上的 /etc/kubernetes/manifests/kube-scheduler.yaml 和 /etc/kubernetes/manifests/kube-controller-manager.yaml 文件中的- –port=0注释掉,然后重启一下各Master节点上的kubelet即可.
测试集群
1 | kubectl create deployment nginx --image nginx --replicas 2 |
参考
如何用 Kubeadm 在 Debian 11 上安装 Kubernetes 集群 | Linux 中国 - 知乎 (zhihu.com)
Kubernetes多主多从高可用集群部署 - 个人文章 - SegmentFault 思否
搭建多主节点k8s高可用集群(三主两从一VIP)_kubernetes部署多主多从集群-CSDN博客
github - 基于Ubuntu22.04部署KubeEdge-v1.18.0环境 - 云原生_KubeEdge - SegmentFault 思否
Git子库
原文链接:Git应用详解第十讲:Git子库:submodule与subtree
一个中大型项目往往会依赖几个模块,git
提供了子库的概念。可以将这些子模块存放在不同的仓库中,通过submodule
或subtree
实现仓库的嵌套。本讲为Git
应用详解的倒数第二讲,胜利离我们不远了!
一、submodule
submodule
:子模块的意思,表示将一个版本库作为子库引入到另一个版本库中:

1.引入子库
需要使用如下命令:
git submodule add 子库地址 保存目录
比如:
1 | git submodule add git@github.com:AhuntSun/git_child.git mymodule |
执行上述命令会将地址对应的远程仓库作为子库,保存到当前版本库的mymodule
目录下:

随后查看当前版本库的状态:

可以发现新增了两个文件。查看其中的.gitmodules
文件:

可以看到当前文件的路径和子模块的url
,随后将这两个新增文件添加、提交并推送。在当前仓库git_parent
对应的远程仓库中多出了两个文件:

其中mymodule
文件夹上的3bd7f76
对应的是子仓库git_child
中的最新提交:

点击mymodule
文件夹,会自动跳转到子仓库中:

通过上述分析,可以得出结论:两个仓库已经关联起来了,并且仓库git_child
为仓库git_parent
的子仓库;
2.同步子库变化
当被依赖的子版本库发生变化时:在子版本库git_child
中新增文件world.txt
并提交到远程仓库:

这个时候依赖它的父版本库git_parent
要如何感知这一变化呢?
方法一
这个时候git_parent
只需要进入存放子库git_child
的目录mymodule
,执行git pull
就能将子版本库git_child
的更新拉取到本地:

方法二
当父版本库git_parent
依赖的多个子版本库都发生变化时,可以采用如下方法遍历更新所有子库:首先回到版本库主目录,执行以下指令:
1 | git submodule foreach git pull |
该命令会遍历当前版本库所依赖的所有子版本库,并将它们的更新拉取到父版本库git_parent
:

拉取完成后,查看状态,发现mymodule
目录下文件发生了变化,所以需要执行一次添加、提交、推送操作:

3.复制父版本库
如果将使用了submodule
添加依赖了子库的父版本库git_parent
,克隆一份到本地的话。在克隆出来的新版本库git_parent2
中,原父版本库存放依赖子库的目录虽在,但是内容不在:

进入根据git_parent
复制出来的仓库git_parent2
,会发现mymodule
目录为空:

解决方法:可采用多条命令的分步操作,也可以通过参数将多步操作进行合并。
分步操作
这是在执行了clone
操作后的额外操作,还需要做两件事:
手动初始化
submodule
:1
git submodule init
手动拉取依赖的子版本库;:
1
git submodule update --recursive

执行完两步操作后,子版本库中就有内容了。由此完成了git_parent
的克隆;
合并操作
分步操作相对繁琐,还可以通过添加参数的方式,将多步操作进行合并。通过以下指令基于git_parent
克隆一份git_parent3
:
1 | git clone git@github.com:AhuntSun/git_parent.git git_parent3 --recursive |

--recursive
表示递归地克隆git_parent
依赖的所有子版本库。
4.删除子版本库
git
没有提供直接删除submodule
子库的命令,但是我们可以通过其他指令的组合来达到这一目的,分为三步:
将
submodule
从版本库中删除:1
git rm --cache mymodule

git rm
的作用为删除版本库中的文件,并将这一操作纳入暂存区;
- 将
submodule
从工作区中删除;

- 最后将
.gitmodules
目录删除;

完成三步操作后,再进行添加,提交,推送即可完成删除子库的操作:

二、subtree
1.简介
subtree
与submodule
的作用是一样的,但是subtree
出现得比submodule
晚,它的出现是为了弥补submodule
存在的问题:
- 第一:
submodule
不能在父版本库中修改子版本库的代码,只能在子版本库中修改,是单向的; - 第二:
submodule
没有直接删除子版本库的功能;
而subtree
则可以实现双向数据修改。官方推荐使用subtree
替代submodule
。
2.创建子库
首先创建两个版本库:git_subtree_parent
和git_subtree_child
然后在git_subtree_parent
中执行git subtree
会列出该指令的一些常见的参数:

3.建立关联
首先需要给git_subtree_parent
添加一个子库git_subtree_child
:
第一步:添加子库的远程地址:
1 | git remote add subtree-origin git@github.com:AhuntSun/git_subtree_child.git |
添加完成后,父版本库中就有两个远程地址了:

这里的subtree-origin
就代表了远程仓库git_subtree_child
的地址。
第二步:建立依赖关系:
1 | git subtree add --prefix=subtree subtree-origin master --squash |
该命令表示将远程地址为subtree-origin
的,子版本库上master
分支的,文件克隆到subtree
目录下;
注意:是在某一分支(如
master
)上将subtree-origin
代表的远程仓库的某一分支(如master
)作为子库拉取到subtree
文件夹中。可切换到其他分支重复上述操作,也就是说子库的实质就是子分支。
--squash
是可选参数,它的含义是合并,压缩的意思。
- 如果不增加这个参数,则会把远程的子库中指定的分支(这里是
master
)中的提交一个一个地拉取到本地再去创建一个合并提交; - 如果增加了这个参数,会将远程子库指定分支上的多次提交合并压缩成一次提交再拉取到本地,这样拉取到本地的,远程子库中的,指定分支上的,历史提交记录就没有了。

拉取完成后,父版本库中会增添一个subtree
目录,里面是子库的文件,相当于把依赖的子库代码拉取到了本地:

此时查看一下父版本库的提交历史:

会发现其中没有子库李四的提交信息,这是因为--squash
参数将他的提交压缩为一次提交,并由父版本库张三进行合并和提交。所以父版本库多出了两次提交。
随后,我们在父版本库中进行一次推送:

结果远程仓库中多出了一个存放子版本库文件的subtree
目录,并且完全脱离了版本库git_subtree_child
,仅仅是属于父版本库git_subtree_parent
的一个目录。而不像使用submodule
那样,是一个点击就会自动跳转到依赖子库的指针:
subtree
的远程父版本库:

submodule
的远程父版本库:

即submodule
与subtree
子库的区别为:

4.同步子库变化
在子库中创建一个新文件world
并推送到远程子库:

在父库中通过如下指令更新依赖的子库内容:
1 | git subtree pull --prefix=subtree subtree-origin master --squash |

此时查看一下提交历史:

发现没有子库李四的提交信息,这都是--squash
的作用。子库的修改交由父库来提交。
5.参数--squash
该参数的作用为:防止子库指定分支上的提交历史污染父版本库。比如在子库的master
分支上进行了三次提交分别为:a
、b
、c
,并推送到远程子库。
首先,复习一下合并分支时遵循的三方合并原则:

当提交4
和6
需要合并的时候,git
会先寻找二者的公共父提交节点,如图中的2
,然后在提交2
的基础上进行2
、4
、6
的三方合并,合并后得到提交7
。
父仓库执行pull
操作时:如果添加参数--squash
,就会把远程子库master
分支上的这三次提交合并为一次新的提交abc
;随后再与父仓库中子库的master
分支进行合并,又产生一次提交X
。整个pull
的过程一共产生了五次提交,如下图所示:

存在的问题:
由于--squash
指令的合并操作,会导致远程master
分支上的合并提交abc
与本地master
分支上的最新提交2
,找不到公共父节点,从而合并失败。同时push
操作也会出现额外的问题。
最佳实践:要么全部操作都使用--squash
指令,要么全部操作都不使用该参数,这样就不会出错。
错误示范:
为了验证,重新创建两个仓库A
和B
,并通过subtree
将B
设置为A
的子库。这次全程都没有使用参数--squash
,重复上述操作:
- 首先,修改子库文件;
- 然后,通过下列指令,在不使用参数
--squash
的情况下,将远程子库A
变化的文件拉取到本地:
1 | git subtree pull --prefix=subtree subtree-origin master |

此时查看提交历史:

可以看到子库儿子
的提交信息污染了父版本库的提交信息,验证了上述的结论。
所以要么都使用该指令,要么都不使用才能避免错误;如果不需要子库的提交日志,推荐使用--squash
指令。
补充:
echo 'new line' >> test.txt
:表示在test.txt
文件末尾追加文本new line
;如果是一个>
表示替换掉test.txt
内的全部内容。
6.修改子库
subtree
的强大之处在于,它可以在父版本库中修改依赖的子版本库。以下为演示:
进入父版本库存放子库的subtree
目录,修改子库文件child.txt
,并推送到远程父仓库:

此时远程父版本库中存放子库文件的subtree
目录发生了变化,但是独立的远程子库git_subtree_child
并没有发生变化。
修改独立的远程子库:
可执行以下命令,同步地修改远程子版本库:
1
git subtree push --prefix=subtree subtree-origin master
如下图所示,父库中的子库文件
child.txt
新增的child2
内容,同步到了独立的远程子库中:修改独立的本地子库:
回到本地子库
git_subtree_child
,将对应的远程子库进行的修改拉取到本地进行合并同步:由此无论是远程的还是本地的子库都被修改了。
实际上使用
subtree
后,在外部看起来父仓库和子仓库是一个整体的仓库。执行clone
操作时,不会像submodule
那样需要遍历子库来单独克隆。而是可以将整个父仓库和它所依赖的子库当做一个整体进行克隆。
存在的问题
父版本库拉取远程子库进行更新同步会出现的问题:
子仓库第一次修改:
经历了上述操作,本地子库与远程子库的文件达到了同步,其中文件
child.txt
的内容都是child~4
。在此基础上本地子库为该文件添加child5~6
:然后推送到远程子库。
父仓库第一次拉取:
随后父版本库通过下述指令,拉取远程子库,与本地父仓库
git_subtree_parent
中的子库进行同步:1
git subtree pull --p subtree subtree-origin master --squash
结果出现了合并失败的情况:
我们查看冲突产生的文件:
发现父版本库中的子库与远程子库内容上并无冲突,但是却发生了冲突,这是为什么呢?
探究冲突产生的原因之前我们先解决冲突,先删除多余的内容:
随后执行
git add
命令和git commit
命令标识解决了冲突:解决完冲突后将该文件推送到独立的远程子库,发现文件并没有发生更新,也就是说
git
认为我们并没有解决冲突:子仓库第二次修改与父仓库第二次拉取:
再次修改本地子库的文件并推送到对应的远程仓库,父版本库再次将远程子库更新的文件拉取到本地进行同步:
这次却成功了!为什么同样的操作,有的时候成功有的时候失败呢?
解决方案
原因出现在--squash
指令中。实际上,--squash
指令把子库中的提交信息合并了,导致父仓库在执行git pull
操作时找不到公共的父节点,从而导致即使文件没有冲突的内容,也会出现合并冲突的情况。其实不使用--squash
也会有这种问题,问题的根本原因仍然是三方合并时找不到公共父节点。我们打开gitk
:

从图中不难看出,当使用subtree
时,子库与父库之间是没有公共节点的,所以时常会因为找不到公共节点而出现合并冲突的情况,此时只需要解决冲突,手动合并即可。
不使用
subtree
时,普通的版本库中的各分支总会有一个公共节点:![]()
再次强调:使用--squash
指令时一定要小心,要么都使用它,要么都不使用。
7.抽离子库
git subtree split
当开发过程中出现某些子库完全可以复用到其他项目中时,我们希望将它独立出来。
方法一:可以手动将文件拷贝出来。缺点是,这样会丢失关于该子库的提交记录;
方法二:
使用
1
git subtree split
指令,该指令会把关于独立出来的子库的每次提交都记录起来。但是,这样存在弊端:
- 比如该独立子库为
company.util
,当一次提交同时修改了company.util
和company.server
两个子库时。 - 通过上述命令独立出来的子库
util
只会记录对自身修改的提交,而不会记录对company.server
的修改,这样在别人看来这次提交就只修改了util
,这是不完整的。
- 比如该独立子库为
C#时间戳
来源:https://blog.guoqianfan.com/2019/11/24/timestamp-in-csharp/
什么是时间戳
时间戳默认是Unix时间戳。
首先要清楚JavaScript与Unix的时间戳的区别:
JavaScript时间戳:是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总毫秒数。
Unix时间戳:是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。
可以看出JavaScript时间戳是总毫秒数,Unix时间戳是总秒数。
比如同样是的 2016/11/03 12:30:00 ,转换为JavaScript时间戳为 1478147400000;转换为Unix时间戳为 1478147400。
从上面也可以看出时间戳与时区无关。
Unix时间戳相互转换
C# DateTime转换为Unix时间戳
.NET 4.6新方法
只能在 .NET 4.6及更高版本里才能使用。
1 | long timeStamp = DateTimeOffset.Now.ToUnixTimeSeconds(); |
通用的老方法
1 | System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1)); |
Unix时间戳转换为C# DateTime
.NET 4.6新方法
由时间戳转换的DateTimeOffset
的时区默认是+00:00
,此时我们需要转为本地时区,否则后续使用可能会有问题。
转为本地时区:DateTimeOffset.LocalDateTime
。
示例代码如下:
1 |
|
通用的老方法
1 | long unixTimeStamp = 1478162177; |
备注
DateTimeOffset使用Now还是UtcNow
对于DateTimeOffset
,发现有2个获取当前时间的属性:DateTimeOffset.Now
和DateTimeOffset.UtcNow
。
如果只是获取时间戳,这2个使用哪个都可以,得到的值是一样的。
因为DateTimeOffset
里面有时区信息,获取时间戳时会使用时区进行转换的,所以获得的时间戳一样。
而也是因为时区的原因,DateTimeOffset
的其他操作可能会不一样。例如DateTimeOffset.DateTime
就不一样,此时推荐使用DateTimeOffset.LocalDateTime
来获得本地时区的时间。
测试代码如下:
1 |
|
DateTime转换为DateTimeOffset
可以直接把DateTime
赋值给DateTimeOffset
,内部会自动进行隐式转换。这里涉及到时区,请往下看。
DateTime的时区信息(Kind属性)
DateTime
的时区信息存放在Kind
属性里。Kind
属性的数据类型是DateTimeKind
枚举,只有3个值:
Unspecified
:未指定/未规定Utc
:UTC
时间Local
:本地时区
不同情况下得到的DateTime
的Kind
是不同的,具体如下:
DateTime.Now
:DateTime.Kind
是 **Local
(本地时区)**。DateTime.UtcNow
:DateTime.Kind
是 **Utc
**。DateTime.Parse()
:【默认】在未指定时区时,
DateTime.Kind
是Unspecified
指定时区:指定时区后
DateTime.Kind
就是相对应的值。指定时区有2种方式:
- 【默认+优先】待转换的字符串里有时区信息。例如:
2019/11/24 17:40:32 +08:00
- 使用
DateTimeStyles
参数来指定时区。DateTimeStyles
是枚举类型,更多信息自己查看定义,这里不再多说。
- 【默认+优先】待转换的字符串里有时区信息。例如:
Local
和Utc
都会把相应的时区传递过去。对于 Unspecified
(未指定),会被当做本地时区来处理(结果已验证,源码没看懂)。
测试代码
1 |
|
输出结果如下:
1 | DateTime: |
DateTimeOffset.Parse的默认时区
DateTimeOffset.Parse
的默认时区是当前时区。
1 |
|
参考
- C# DateTime与时间戳转换:https://www.cnblogs.com/polk6/p/6024892.html
- 如何将Unix时间戳转换为DateTime,反之亦然?:https://stackoverflow.com/questions/249760/how-can-i-convert-a-unix-timestamp-to-datetime-and-vice-versa
- DateTimeOffset源码:https://source.dot.net/#System.Private.CoreLib/DateTimeOffset.cs
8种主要排序算法的CSharp实现
8种主要排序算法的C#实现
Excerpt
8种主要排序算法的实现及优化,包含选择排序,冒泡排序,插入排序,快速排序,归并排序,堆排序,希尔排序,基数排序。文末实际测试并比较。
新的一年到了,很多园友都辞职要去追求更好的工作环境,我也是其中一个,呵呵!
最近闲暇的时候我开始重温一些常用的算法。老早就买了《算法导论》,一直都没啃下去。
这本书确实很好,只是太难读了,总是读了几章就又读不下去了!工作上也几乎用不到。
我这段时间发现看这些排序算法比以前容易了很多,就借此机会将它们整理总结起来。
一是方便以后重温,二是可以应对笔试面试。同时也希望这篇博文可以帮助各位刚辞职和正在学习排序算法的园友。
PS:有可能实现的代码并不是最优的,如果有什么错误或者值得改进的地方,还请大家帮忙指出。
简介
排序算法是我们编程中遇到的最多的算法。目前主流的算法有8种。
平均时间复杂度从高到低依次是:
冒泡排序(o(n2)),选择排序(o(n2)),插入排序(o(n2)),堆排序(o(nlogn)),
归并排序(o(nlogn)),快速排序(o(nlogn)), 希尔排序(o(n1.25)),基数排序(o(n))
这些平均时间复杂度是参照维基百科排序算法罗列的。
是计算的理论平均值,并不意味着你的代码实现能达到这样的程度。
例如希尔排序,时间复杂度是由选择的步长决定的。基数排序时间复杂度最小,
但我实现的基数排序的速度并不是最快的,后面的结果测试图可以看到。
本文代码实现使用的数据源类型为IList
List
选择排序
选择排序是我觉得最简单暴力的排序方式了。
以前刚接触排序算法的时候,感觉算法太多搞不清,唯独记得选择排序的做法及实现。
原理:找出参与排序的数组最大值,放到末尾(或找到最小值放到开头) 维基入口
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> <span>void</span> SelectSort(IList<<span>int</span>><span> data) |

过程解析:将剩余数组的最小数交换到开头。
冒泡排序
冒泡排序是笔试面试经常考的内容,虽然它是这些算法里排序速度最慢的(汗),后面有测试为证。
原理:从头开始,每一个元素和它的下一个元素比较,如果它大,就将它与比较的元素交换,否则不动。
这意味着,大的元素总是在向后慢慢移动直到遇到比它更大的元素。所以每一轮交换完成都能将最大值
冒到最后。 维基入口
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> <span>void</span> BubbleSort(IList<<span>int</span>><span> data) |

过程解析:中需要注意的是j<i,每轮冒完泡必然会将最大值排到数组末尾,所以需要排序的数应该是在减少的。
很多网上版本每轮冒完泡后依然还是将所有的数进行第二轮冒泡即j<data.Count-1,这样会增加比较次数。
通过标识提升冒泡排序
在维基上看到,可以通过添加标识来分辨剩余的数是否已经有序来减少比较次数。感觉很有意思,可以试试。
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> <span>void</span> BubbleSortImprovedWithFlag(IList<<span>int</span>><span> data) |

过程解析:发现某轮冒泡没有任何数进行交换(即已经有序),就跳出排序。
我起初也以为这个方法是应该有不错效果的,可是实际测试结果并不如想的那样。和未优化耗费时间一样(对于随机数列)。
由果推因,那么应该是冒泡排序对于随机数列,当剩余数列有序的时候,也没几个数要排列了!?
不过如果已经是有序数列或者部分有序的话,这个冒泡方法将会提升很大速度。
鸡尾酒排序(来回排序)
对冒泡排序进行更大的优化
冒泡排序只是单向冒泡,而鸡尾酒来回反复双向冒泡。
原理:自左向右将大数冒到末尾,然后将剩余数列再自右向左将小数冒到开头,如此循环往复。维基入口
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> <span>void</span> BubbleCocktailSort(IList<<span>int</span>><span> data) |

过程解析:分析第i轮冒泡,i是偶数则将剩余数列最大值向右冒泡至末尾,是奇数则将剩余数列最小值
向左冒泡至开头。对于剩余数列,n为始,data.Count-1-m为末。
来回冒泡比单向冒泡:对于随机数列,更容易得到有序的剩余数列。因此这里使用标识将会提升的更加明显。
插入排序
插入排序是一种对于有序数列高效的排序。非常聪明的排序。只是对于随机数列,效率一般,交换的频率高。
原理:通过构建有序数列,将未排序的数从后向前比较,找到合适位置并插入。维基入口
第一个数当作有序数列。
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> <span>void</span> InsertSort(IList<<span>int</span>><span> data) |

过程解析:将要排序的数(索引为i)存储起来,向前查找合适位置j+1,将i-1到j+1的元素依次向后
移动一位,空出j+1,然后将之前存储的值放在这个位置。
这个方法写的不如维基上的简洁清晰,由于合适位置是j+1所以多出了对j==0的判断,但实际效率影响无差别。
建议比照维基和我写的排序,自行选择。
二分查找法优化插入排序
插入排序主要工作是在有序的数列中对要排序的数查找合适的位置,而查找里面经典的二分查找法正可以适用。
原理:通过二分查找法的方式找到一个位置索引。当要排序的数插入这个位置时,大于前一个数,小于后一个数。
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> <span>void</span> InsertSortImprovedWithBinarySearch(IList<<span>int</span>><span> data) |

过程解析:需要注意的是二分查找方法实现中high-low==1的时候mid==low,所以需要33行
mid-1<0即mid==0的判断,否则下行会索引越界。
快速排序
快速排序是一种有效比较较多的高效排序。它包含了“分而治之”以及“哨兵”的思想。
原理:从数列中挑选一个数作为“哨兵”,使比它小的放在它的左侧,比它大的放在它的右侧。将要排序是数列递归地分割到
最小数列,每次都让分割出的数列符合“哨兵”的规则,自然就将数列变得有序。 维基入口
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> <span>void</span> QuickSortStrict(IList<<span>int</span>><span> data) |

过程解析:取的哨兵是数列的第一个值,然后从第二个和末尾同时查找,左侧要显示的是小于哨兵的值,
所以要找到不小于的i,右侧要显示的是大于哨兵的值,所以要找到不大于的j。将找到的i和j的数交换,
这样可以减少交换次数。i>=j时,数列全部查找了一遍,而不符合条件j必然是在小的那一边,而哨兵
是第一个数,位置本应是小于自己的数。所以将哨兵与j交换,使符合“哨兵”的规则。
这个版本的缺点在于如果是有序数列排序的话,递归次数会很可怕的。
另一个版本
这是维基上的一个C#版本,我觉得很有意思。这个版本并没有严格符合“哨兵”的规则。但却将“分而治之”
以及“哨兵”思想融入其中,代码简洁。
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> <span>void</span> QuickSortRelax(IList<<span>int</span>><span> data) |

过程解析:取的哨兵是数列中间的数。将数列分成两波,左侧小于等于哨兵,右侧大于等于哨兵。
也就是说,哨兵不一定处于两波数的中间。虽然哨兵不在中间,但不妨碍“哨兵”的思想的实现。所以
这个实现也可以达到快速排序的效果。但却造成了每次递归完成,要排序的数列数总和没有减少(除非i==j)。
针对这个版本的缺点,我进行了优化
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> <span>void</span> QuickSortRelaxImproved(IList<<span>int</span>><span> data) |

过程解析:定义了一个变量Index,来跟踪哨兵的位置。发现哨兵最后在小于自己的那堆,
那就与j交换,否则与i交换。达到每次递归都能减少要排序的数列数总和的目的。
归并排序
归并排序也是采用“分而治之”的方式。刚发现分治法是一种算法范式,我还一直以为是一种需要意会的思想呢。
不好意思了,孤陋寡闻了,哈哈!
原理:将两个有序的数列,通过比较,合并为一个有序数列。 维基入口
为方便理解,此处实现用了List
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> List<<span>int</span>> MergeSortOnlyList(List<<span>int</span>> data, <span>int</span> low, <span>int</span><span> high) |

过程解析:将数列分为两部分,分别得到两部分数列的有序版本,然后逐个比较,将比较出的小数逐个放进
新的空数列中。当一个数列放完后,将另一个数列剩余数全部放进去。
IList版本
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> IList<<span>int</span>> MergeSort(IList<<span>int</span>><span> data) |

过程原理与上个一样,此处就不赘述了。
堆排序
堆排序是根据堆这种数据结构设计的一种算法。堆的特性:父节点的值总是小于(或大于)它的子节点。近似二叉树。
原理:将数列构建为最大堆数列(即父节点总是最大值),将最大值(即根节点)交换到数列末尾。这样要排序的数列数总和减少,
同时根节点不再是最大值,调整最大堆数列。如此重复,最后得到有序数列。 维基入口 有趣的演示
实现准备:如何将数列构造为堆——父节点i的左子节点为2i+1,右子节点为2i+2。节点i的父节点为floor((i-1)/2)。
实现如下(这个实现判断和临时变量使用太多,导致效率低,评论中@小城故事提出了更好的实现):

1 | <span> 1</span> <span>public</span> <span>static</span> <span>void</span> HeapSort(IList<<span>int</span>><span> data) |

过程解析:BuildMaxHeapify为排序前构建的最大堆数列方法,主要内容为从最后一个父节点开始往前将每个三角组合
(即父节点与它的两个子节点)符合父节点值最大的规则。ReSortMaxBranch为将三角调整为父节点值最大,
并返回该值之前的索引,用来判断是否进行了交换,以及原来的父节点值交换到了什么位置。在HeapSort里首先
构建了最大堆数列,然后将根节点交换到末尾,根节点不是最大值了,在while语句中对最大堆数列进行调整。
插曲:自从看了Martin Fowler大师《重构》第三版,我发现我更不喜欢写注释了。每次都想着尽量让方法的名字更贴切,
即使会造成方法的名字很长很丑。这算不算曲解了大师的意思啊!?上面的代码注释都是写博客的时候现加的(源代码很干净的。汗!)。
希尔排序
希尔排序是插入排序的一种更高效的改进版本。
在前面介绍的插入排序,我们知道1.它对有序数列排序的效率是非常高的 2.要排序的数向前移动是一步步进行的导致插入排序效率低。
希尔排序正是利用第一点,改善第二点,达到更理想的效果。
原理:通过奇妙的步长,插入排序间隔步长的元素,随后逐渐缩短步长至1,实现数列的插入排序。 维基入口
疑问:可以想象到排序间隔步长的数,会逐渐让数列变得有序,提升最后步长为1时标准插入排序的效率。在维基上看到这么
一句话“可能希尔排序最重要的地方在于当用较小步长排序后,以前用的较大步长仍然是有序的”注意用词是‘可能’。我的疑问是
这是个正确的命题吗?如何证明呢?看维基上也是由果推因,说是如果不是这样,就不会排序那么快了。可这我感觉还是太牵强了,
哪位大哥发现相关资料,希望能分享出来,不胜感激。
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> <span>void</span> ShellSort(IList<<span>int</span>><span> data) |

过程解析:采用的步长是N/2,每次取半,直至1。循环内部就是标准的插入排序。
——————
修正:修正后希尔排序才是真正牛叉的希尔啊!感谢@390218462的提出

1 | <span> 1</span> <span>public</span> <span>static</span> <span>void</span> ShellSortCorrect(IList<<span>int</span>><span> data) |



——————
这里实现的貌似是最差的希尔排序。主要源于步长的选择。维基上有各种牛叉的“凌波微步”,极限在哪里,
喜欢挑战的同学可以去学习学习。看维基排序算法里六种排序的测试,希尔最快,比快速排序还快!!我没实现啊!
只是对于神奇的步长更充满了敬畏。
基数排序
基数排序是一种非比较型整数排序。
“非比较型”是什么意思呢?因为它内部使用的是桶排序,而桶排序是非比较型排序。
这里就要说说桶排序了。一个非常有意思的排序。
桶排序
原理:取一定数量(数列中的最大值)的编好序号的桶,将数列每个数放进编号为它的桶里,然后将不是空的桶依次倒出来,
就组成有序数列了。 维基入口
好吧!聪明的人一眼就看出桶排序的破绽了。假设只有两个数1,10000,岂不是要一万个桶!?这确实是个问题啊!我也
没想出解决办法。我起初也以为桶排序就是一个通过牺牲空间来换取时间的排序算法,它不需要比较,所以是非比较型算法。
但看了有趣的演示的桶排序后,发现世界之大,你没有解决,不代表别人没解决,睿智的人总是很多。
1,9999的桶排序实现:new Int[2];总共有两个数,得出最大数9999的位数4,取10的4次幂即10000作为分母,
要排序的数(1或9999)作为分子,并乘以数列总数2,即1*2/10000,9999*2/10000得到各自的位置0,1,完成排序。
如果是1,10000进行排序的话,上面的做法就需要稍微加一些处理——发现最大数是10的n次幂,就将它作为分母,并
放在数列末尾就好了。
如果是9999,10000进行排序的话,那就需要二维数组了,两个都在位置1,位置0没数。这个时候就需要在放
入每个位置时采用其它排序(比如插入排序)办法对这个位置的多个数排序了。
为基数排序做个过渡,我这里实现了一个个位数桶排序
涉及到了当重复的数出现的处理。
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> <span>void</span> BucketSortOnlyUnitDigit(IList<<span>int</span>><span> data) |

过程解析:indexCounter进行对每个数出现的频率的统计。indexBegin存储每个数的起始索引。
比如 1 1 2,indexCounter统计到0个0,2个1,1个2。indexBegin计算出0,1,2的起始索引分别为
0,0,2。当1个1已取出排序,那索引将+1,变为0,1,2。这样就通过提前给重复的数空出位置,解决了
重复的数出现的问题。当然,你也可以考虑用二维数组来解决重复。
下面继续基数排序。
基数排序原理:将整数按位数切割成不同的数字,然后按每个位数分别比较。
取得最大数的位数,从低位开始,每个位上进行桶排序。
实现如下:

1 | <span> 1</span> <span>public</span> <span>static</span> IList<<span>int</span>> RadixSort(IList<<span>int</span>><span> data) |

过程解析:得出最大数的位数,从低位开始桶排序。我写的这个实现代码并不简洁,但逻辑更清晰。
后面测试的时候我们就会发现,按理来说这个实现也还行吧! 但并不如想象的那么快!
循环的次数太多?(统计频率n次+9次计算+n次放到新的数组)*位数。
创建的新实例太多?(new int[10]两次+NewInstance is反射判断创建实例+new int[n])*位数
测试比较
添加随机数组,数组有序校验,微软Linq排序
代码如下:

1 | <span> 1</span> <span>public</span> <span>static</span> <span>int</span>[] RandomSet(<span>int</span> length, <span>int</span><span> max) |

测试主体如下:

1 | <span> 1</span> <span>static</span> <span>void</span> Main(<span>string</span><span>[] args) |

剩余代码折叠在此处

测试设备:win8(64位),i7-3630QM,8G内存,vs2012
测试结果:
100000,50000,10000,5000,1000,100依次是:






结果分析:可以看出在大数组的时候,微软自带排序更接近快速排序。而当数组变小时,速度却没有明显提升,甚至变得更慢,
比如1000和100。可以推断出在数组足够小的时候,比较已经不是影响这个方法主要因素。而根据它对大数组的表现。我们可以
推断出它应该用的是快速排序。反编译验证下:

在System.Linq.EnumerableSorter下。有兴趣的同学可以去看下详细实现。
维基上也有个测试。硬件没我的好。时间是我测试结果时间的几百倍。有兴趣的同学可以比较下。
在上面的测试中,我们可以看到快速最快,归并其次,冒泡最慢(维基上是希尔最快,估计使用的是某种神奇的步长)。
在我这里,以前实现的希尔还不如二分查找优化版的快,修正后希尔快了相当多,上面测试的希尔排序是以前错误的实现。
修正后的实现测试效果请点击右侧导航到希尔排序查看。希尔排序是一种神奇又有潜力的算法。步长不好会很挫!
而基数排序却是比平均时间复杂度为o(nlogn)的堆排序,归并排序,快速排序还要慢的,虽然它的平均时间复杂度为o(n)。
冒泡标识优化版对随机数列结果优化不明显,鸡尾酒版优化可以看到,但也不是很厉害。
插入排序二分查找优化版优化比较明显。我优化的快速排序QuickSortRelaxImproved优化也不明显。
以上是随机数列的测试结果,最大值为99999。
而对于有序数列,这些方法表现又会如何呢?
我这里就不演示了。本文末尾会附上demo,大家可以自行测试。
有意思的是:
我在测试有序数列的时候,QuickSortStrict方法栈溢出了(stack overflow exception)。这个异常
是让我去stackoverflow搜寻答案吗?哈哈!我确信我的方法不是无限循环。跳过一堆链接。。。我是
在测试10000个数排序的时候发生的错误。我跟踪后发现大约在9400多次递归的时候,栈溢出。找啊找
终于找见了一个类似的问题。上面说如果一个递归9000多次而没有返回值,也会报栈溢出的。而这个方法
对于10000个有序数列,确实每次减少一个数地递归,次数会超过限制。
我的算法理论不怎么好,对于时间复杂度和空间复杂度,还有稳定度,搞得也不怎么清楚,只知道个大致的
意思。各位要笔试面试的朋友可以去维基百科这个表来了解学习。
总结
我觉得使用IList
或者int[]来调用微软封装的方法。这样说来,题目说C#实现倒快有点名不副实了。不过这样却也方便了其它语言
朋友。比如将我这篇博文里的实现随便改改,就可以说是另一个语言版本的8种排序算法了。哈哈!在这里,
我想说下这次学习排序对我的意义:老久不怎么动脑了,突然动起来,磨磨唧唧地得出结果,最后倒也有点成就感!
在学习过程中,经常会脑子转不过弯,想不通的,只是走在路上或者睡觉前突然灵感一现,有点小惊喜的感觉!
这大概就是进步的特征吧!哈哈!这次写demo+写博客花费了不少时间,倒也收获颇多,尤其在我将8种
排序都实现之前,没进行过一次测试,全部实现完成后,测试时各种索引越界+无限循环+各种问题,没几个
能跑通的,到后来的几乎都没有问题,也算是锻炼了思维,找出错原因的能力。本篇是自我学习的一个总结,
要学习及锻炼的园友,还望一定自己实现一下,可以和我的比较一下,解除疑惑或者提出改进。
主要参考:维基百科,有趣的演示
Demo源码
PS:我打算三月份去广州发展,主要会Asp.net mvc+jquery(不介意学习新的技术[除了webform]及语言[除了java])。
CSharp排序算法小结
前言
算法这个东西其实在开发中很少用到,特别是web开发中,但是算法也很重要,因为任何的程序,任何的软件,都是由很多的算法和数据结构组成的。但是这不意味着算法对于每个软件设计人员的实际工作都是很重要的。每个项目特点和需求特殊也导致算法运用场景上不同。但是个人觉得算法运用的好的话会给自己在程序设计的时候提供比较好的思路。下面就对一些排序算法小结一下,就当做自己的一个笔记吧。
插入排序
1.简介
插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
2.算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
1.从第一个元素开始,该元素可以认为已经被排序
2.取出下一个元素,在已经排序的元素序列中从后向前扫描
3.如果该元素(已排序)大于新元素,将该元素移到下一位置
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5.将新元素插入到该位置后
6.重复步骤2~5
如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。该算法可以认为是插入排序的一个变种,称为二分查找排序。
3.使用插入排序为一列数字进行排序的过程
最差时间复杂度
最优时间复杂度
平均时间复杂度
4.C#实现
/// <summary>
/// 插入排序 /// </summary>
public class InsertionSorter
{ public void Sort(int\[\] list)
{ for (int i = 1; i < list.Length; ++i)
{ int t = list\[i\]; int j = i; while ((j > 0) && (list\[j - 1\] > t))
{
list\[j\] \= list\[j - 1\]; \--j;
}
list\[j\] \= t;
}
}
}
数组
int[] iArrary = new int[] { 1, 5, 3, 6, 10, 55, 9, 2, 87, 12, 34, 75, 33, 47 };
希尔排序
1.简介
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。
2.算法实现
原始的算法实现在最坏的情况下需要进行O(n2)的比较和交换。V. Pratt的书[1] 对算法进行了少量修改,可以使得性能提升至O(n log2 n)。这比最好的比较算法的O(n log n)要差一些。
希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n2)的排序(冒泡排序或插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。
一个更好理解的希尔排序实现:将数组列在一个表中并对列排序(用插入排序)。重复这过程,不过每次用更长的列来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身仅仅对原数组进行排序(通过增加索引的步长,例如是用i += step_size而不是i++)。
3.排序过程
最差时间复杂度 根据步长串行的不同而不同。
最优时间复杂度 O(n)
平均时间复杂度 根据步长串行的不同而不同。
4.C#实现
/// <summary>
/// 希尔排序 /// </summary>
public class ShellSorter
{ public void Sort(int\[\] list)
{ int inc; for (inc = 1; inc <= list.Length / 9; inc = 3 \* inc + 1) ; for (; inc > 0; inc /= 3)
{ for (int i = inc + 1; i <= list.Length; i += inc)
{ int t = list\[i - 1\]; int j = i; while ((j > inc) && (list\[j - inc - 1\] > t))
{
list\[j \- 1\] = list\[j - inc - 1\];
j \-= inc;
}
list\[j \- 1\] = t;
}
}
}
}
选择排序
1.简介
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
2.实现过程
最差时间复杂度 О(n²)
最优时间复杂度 О(n²)
平均时间复杂度 О(n²)
3.C#实现
/// <summary>
/// 选择排序 /// </summary>
public class SelectionSorter
{ // public enum comp {COMP\_LESS,COMP\_EQUAL,COMP\_GRTR};
private int min; // private int m=0;
public void Sort(int\[\] list)
{ for (int i = 0; i < list.Length - 1; ++i)
{
min \= i; for (int j = i + 1; j < list.Length; ++j)
{ if (list\[j\] < list\[min\])
min \= j;
} int t = list\[min\];
list\[min\] \= list\[i\];
list\[i\] \= t; // Console.WriteLine("{0}",list\[i\]);
}
}
}
冒泡排序
1.简介
冒泡排序(Bubble Sort,台湾译为:泡沫排序或气泡排序)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序对n个项目需要O(n^{2})的比较次数,且可以原地排序。尽管这个算法是最简单了解和实作的排序算法之一,但它对于少数元素之外的数列排序是很没有效率的。
冒泡排序是与插入排序拥有相等的执行时间,但是两种法在需要的交换次数却很大地不同。在最坏的情况,冒泡排序需要O(n^{2})次交换,而插入排序只要最多O(n)交换。冒泡排序的实现(类似下面)通常会对已经排序好的数列拙劣地执行(O(n^{2})),而插入排序在这个例子只需要O(n)个运算。因此很多现代的算法教科书避免使用冒泡排序,而用插入排序取代之。冒泡排序如果能在内部循环第一次执行时,使用一个旗标来表示有无需要交换的可能,也有可能把最好的复杂度降低到O(n)。在这个情况,在已经排序好的数列就无交换的需要。若在每次走访数列时,把走访顺序和比较大小反过来,也可以稍微地改进效率。有时候称为往返排序,因为算法会从数列的一端到另一端之间穿梭往返。
2.算法实现
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3.针对所有的元素重复以上的步骤,除了最后一个。
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
3.实现过程
最差时间复杂度
最优时间复杂度
平均时间复杂度
4.C#实现
/// <summary>
/// 冒泡排序 /// </summary>
public class bubblesort
{ public void BubbleSort(int\[\] R)
{ int i, j, temp; //交换标志
bool exchange; for (i = 0; i < R.Length; i++) //最多做R.Length-1趟排序
{
exchange = false; //本趟排序开始前,交换标志应为假
for (j = R.Length - 2; j >= i; j–)
{ if (R[j + 1] < R[j]) //交换条件
{
temp = R[j + 1];
R[j + 1] = R[j];
R[j] = temp;
exchange = true; //发生了交换,故将交换标志置为真
}
} if (!exchange) //本趟排序未发生交换,提前终止算法
{ break;
}
}
}
}
设计模式系列导航
创建型
创建型模式抽象了实例化的过程。创建性模式隐藏了这些类的实例是如何被创建和放在一起,整个系统关于这些对象所知道的是由抽象类所定义的接口。这样,创建性模式在创建了什么、谁创建它、她是怎么被创建的、以及何时创建方面提供了灵活性。创建相应数目的原型并克隆她们通常比每次用适合的状态手工实例化该类更方便。
单例模式 (Singleton) 保证一个类仅有一个实例,并提供一个访问它的全局访问点。优点:对唯一实例的受控访问。
缺点:饿汉式/懒汉式 多线程同时访问时可能造成多个实例。
工厂方法模式 (Factory Method) 定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。优点:是简单工厂模式的进一步抽象和推广,既保持了简单工厂模式的优点(工厂类中包含了必要的逻辑判断,根据客户端的选择条件动态实例化相关的类。对于客户端来说,去除了与具体产品的依赖),而且克服了简单工厂的缺点(违背了开放封闭原则)。
缺点:每增加一个产品,就需要增加一个产品工厂的类,增加了额外的开发。(用反射可以解决)。
抽象工厂模式 (Abstract Factory) 提供一个创建一系列相关或互相依赖对象的接口,而无需指定它们具体的类。优点:
a) 改变具体工厂即可使用不同的产品配置,使改变一个应用的具体工厂变得很容易。
b) 让具体的创建实例过程与客户端分离,客户端通过抽象接口操作实例,产品的具体类名也被具体工厂的实现分离。
缺点:如果要新增方法,改动极大。
建造者模式 (Builder) 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。优点:使得建造代码与表示代码分离。
缺点:1、增加代码量;2、Builder只是一个替代构造器的选择,不能直接用于降低非构造函数方法的参数数量。
原型模式 (Prototype) 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。优点:隐藏了对象创建的细节,大大提升了性能。不用重新初始化对象,而是动态的获得对象运行时的状态。
缺点:深复制 or 浅复制 。
结构型
适配器模式 (Adapter) 将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。在GoF的设计模式中,适配器有两种类型,类适配器模式和对象适配器模式。
a) 类适配器模式:通过多重继承对一个接口与另一个接口进行匹配,而C#,Java等语言都不支持多重继承,也就是一个类只有一个父类。
b) 一般都指的是 对象适配器模式
优点:能够复用现存的类,客户端统一调用同一接口,更简单、直接、紧凑。
缺点:适配器模式有点儿“亡羊补牢”的感觉,设计阶段要避免使用。
桥接模式 (Bridge) 将抽象部分与它的实现部分分离,使它们都可以独立的变化。优点:减少各部分的耦合。 分离抽象和实现部分,更好的扩展性,可动态地切换实现、可减少子类的个数。
缺点:1、桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。 2、桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性
装饰模式 (Decorator) 动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更灵活。优点:把类中的装饰功能从类中搬移出去,简化原有的类。有效的把类的核心职责和装饰功能区分开,去除相关类中重复的装饰逻辑。
缺点:利用装饰器模式,常常造成设计中有大量的小类,数量实在太多,可能会造成使用此API程序员的困扰。
组合模式 (Composite) 将对象组合成树形结构以表示“部分-整体”的层次结构。优点:组合模式让客户可以一致的使用组合结构和单个对象。
缺点:使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性,而且不是所有的方法都与叶子对象子类都有关联。
外观模式 (Facade) 为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。优点:1、客户对子系统的使用变得简单了,减少了与子系统的关联对象,实现了子系统与客户之间的松耦合关系。 2、只是提供了一个访问子系统的统一入口,并不影响用户直接使用子系统类 3、降低了大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程。
缺点:1、不能很好地限制客户使用子系统类,如果对客户访问子系统类做太多的限制则减少了可变性和灵活性 2、在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
享元模式 (Flyweight) 运用共享技术有效的支持大量细粒度的对象。优点:享元模式可以避免大量非常相似类的开销。程序中,大量细粒度的类实例来表示数据,如果它们除了几个参数外基本相同,那么把它们转移到类实例的外面,在方法调用时将它们传递进来,就可以通过共享大幅度减少单个实例的数目。
缺点:1、由于享元模式需要区分外部状态和内部状态,使得应用程序在某种程度上来说更加复杂化了。2、为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
代理模式 (Proxy) 为其他对象提供一种代理以控制对这个对象的访问。优点:1)代理模式能将代理对象与真正被调用的对象分离,在一定程度上降低了系统的耦合度。2)代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用。代理对象也可以对目标对象调用之前进行其他操作。
缺点:1)在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢。2)增加了系统的复杂度。
行为型
模板方法 (Template Method) 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。优点:模板方法模式是通过把不变行为搬移到超类,去除子类中重复代码来实现它的优势,提供了一个代码复用平台,帮助子类摆脱重复的不变行为的纠缠。
缺点:如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大。
命令模式 (Command) 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。优点:
a) 命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开。
b) 它能较容易的设计一个命令队列。
c) 在需要的情况下,可以较容易的将命令记入日志。
d) 允许接收请求的一方决定是否要否决请求。
e) 可以容易的实现对请求的撤销和重做。
f) 由于加进新的具体命令类不影响其他类,因此增加新的具体命令类很容易。
缺点:会增加系统的复杂性,这里的复杂性应该主要指的是类的数量。
迭代器模式 (Iterator) 提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。优点:迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器来负责,这样既可以做到不暴露集合的内部结构,又可以让外部代码透明的访问集合内部的数据。
缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
观察者模式 (Publish/Subscribe) 定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,让它们能够自动更新自己。优点:解耦。
缺点:如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃。在使用观察者模式是要特别注意这一点。
中介者模式 (mediator) 用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显示的相互引用,从而使其耦合松散,而且可以独立的改变它们之间的交互。优点:
a) 抽象中介者类(Mediator)减少了抽象同事类(colleague)之间的耦合,是的可以独立的改变和复用各个类。
b) 由于把对象如何协作进行了抽象,将中介作为一个独立的概念并将其封装在一个对象中,这样关注的对象就从对象各自本身的行为转移到它们之间的交互上来,也就是站在一个更宏观的角度去看待系统。
缺点:控制集中化导致了中介者的复杂化。
状态模式 (State) 当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是改变了其类。优点:状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化。【消除庞大的条件分支语句】。
缺点:违背开放-封闭原则
策略模式 (strategy) 它定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化不会影响到使用算法的用户。优点:策略模式的策略类为上下文定义了一系列可供重用的算法或行为,继承有助于析取出这些算法中的公共功能。另外,策略模式简化了单元测试,因为每一个算法都有自己的类,可以通过自己的接口单独测试。当不同的行为堆砌在一个类中,很难避免使用switch语句。但是将这些行为封装在一个一个独立的策略类中,可以在使用这些行为的类中消除条件语句
缺点:基本的策略模式,选择权在客户端,具体实现转给策略模式的上下文对象。这并不好。使用策略模式和工厂类结合,可以减轻客户端的职责。但是还是不够完美,使用反射才能真正快乐。
责任链模式 (chain of responsibility) 使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。优点:使得接收者和发送者都没有对方的明确信息,且链中对象自己也不知道链结构,结果是职责链可以简化对象的相互连接,它们只需要保持一个指向其后继者的引用,而不需要保持它所有的候选接收者的引用。开发者可以随时的增加或者修改处理一个请求的结构,增强了给对象指派职责的灵活性。
缺点:一个请求极有可能到了链的末端都得不到处理,或者因为没有正确配置而得不到处理。
访问者模式 (Vistor) 表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。优点:增加新的操作很容易。新的操作就是新的访问者。
缺点:很难增加新的数据结构。
备忘录模式 (Memento) 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。优点:使用备忘录模式可以把复杂的发起人内部信息对其他的对象屏蔽起来,从而可以恰当地保持封装的边界。
缺点:如果发起人角色的状态需要完整地存储到备忘录对象中,那么在资源消耗上面备忘录对象会很昂贵。
解释器模式 (interpreter) 给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。优点:解释器很容易改变和扩展文法,因为该模式使用类来表示文法规则,可以使用继承来改变或扩展文法,也比较容易实现文法。因为定义抽象语法树中各个节点的类的实现大体类似,这些类都易于直接编写。
缺点:解释器模式为文法中的每一条规则至少定义了一个类,因此包含许多规则的文法可能难以管理和维护,建议当文法非常复杂时,使用其他技术(语法分析程序、编译器生成器)。