《算法导论》笔记——循环不变式及插入排序证明

算法导论第二章中提出了一个概念--“循环不变式”

那么,何为循环不变式

我的理解是:

“循环不变式是用于证明算法正确性的一种工具”

它应该怎么用呢
  • 首先,对于任意的一种算法,我们需要找出其循环不变式
  • 然后,需要证明循环不变式的三条性质
对于插入排序算法,它的证明是这样的:
  • 设下标j为目前正在排序的数字的索引,开始时j = 1(C++中索引从0开始,我们这里从第二个元素开始排,至于为什么,请往下读)
  • 循环不变式:“for循环每次开始时,A[0…j-1]是有序的”
接下来我们证明三条性质:
  • 初始化 :循环的第⼀次迭代之前,循环不变式为真
  • 保持 :如果循环的某次迭代之前循环不变式为真,那么下次迭代之前它仍为真
  • 终止 :终止时不变式要能做到符合结果(比如:完成排序)
证明:

1.初始化:显然,j = 1时,A[0…j-1]就是A[0],只有一个数字,当然是符合循环不变式的

2.保持:若 A[0..j-1] 有序,将 A[j] 插入后,A[0..j] 仍有序

3.终止:终止时,当 j = n 时,A[0..n] 整体有序,排序完成

因此你可以看到,事实上,循环不变式就是用来规范证明算法正确性的工具

如果你对数学归纳法比较熟的话,很容易发现其实循环不变式就是数学归纳法的一个变种

如何找循环不变式?

由于算法是一步步执行的,那么如果每一步(包括初试和结束)都满足一个共同的条件,那么这个条件就是要找的循环不变式(loop invariant)

一个例子:

二分查找

不变式:若目标值存在,则必在子数组 A[l..r] 中

证明要点:

初始化:l=0, r=n-1,覆盖整个数组

保持:根据中间值比较调整边界,目标值仍在新区间内

终止:l > r 时子数组为空,目标不存在 → 返回 -1


我们发现,三条性质都符合我们的要求(初始化和保持满足不变式、终止符合要求的结果),所以算法正确

循环不变式不只是理论工具——它强迫你在写循环时明确“我试图维护什么”。这种思维习惯能显著减少代码错误。

——《算法导论》核心思想

插入排序

算法原理:

插入排序与我们手动整理一副牌的过程类似(这里我们引用算法导论上的比喻)

(注意,目前为了易读,我们讨论升序排序,对于非升序,参考GitHub:《算法导论》笔记src/insertion_sort(插入排序).h中的注释

想象一下:你现在右手放着一副乱序的牌,左手开始时什么都没有
我们规定:右手的牌是无序的,左手始终有序
那么,我们现在从右手拿出一张牌,放到左手中
此时,你左手的牌仍然有序,因为此时只有一张牌,符合规定

接着,我们去拿下一张牌,这时有两种情况:
1.当前的牌面>=左手牌面
2.当前的牌面<左手牌面
对于1,我们直接将牌放于左手的顶部即可
对于2,我们需要将牌放于左手第一张的下面


让我们重复这个过程:
1.取出一张牌,我们记录它的值为key
2.从左手的第一张开始,逐个比较(我们记为A[j]),直到key<=A[j],执行3;
3.那么此时,A[j+1]就是key这个值在左手上的正确位置,我们令A[j+1]=key,
相当于把key插入到对应位置
4.执行1-3,直到整个数列有序

问题是,在计算机中,我们没办法执行/* by 01130.hk - online tools website : 01130.hk/zh/webstatus.html */ 插入这个操作,更具体的说,令A[j+1]=key时,会丢失A[j+1]的值

此时想想排牌时的操作:当我们找到合适位置时,我们会将它右侧的所有牌右移一点,/* by 01130.hk - online tools website : 01130.hk/zh/webstatus.html */ 腾出一个空位来

那么在算法中,我们应该怎么办呢?

很简单,我们做一点点修改:

在上述的过程2中,如果我们发现key>A[j],我们令A[j+1] = A[j]

可以试验一下:
假设左手牌为:2,4
当前的key = 3
当前的j = 1(C++索引从0开始)
执行2:由于4>key,我们让A[j+1] = A[j],即A[2] = A[1]

那么,此时A[j]已经复制到了A[j+1],我们无论怎么操作都不会丢失A[j]的数据,相当于腾出来了一个空位

继续执行2: 此时的j = 0,A[j] = A[0] = 2<key,执行3

执行3: 此时,A[j+1]就是key在左手的正确位置,所以令A[j+1]=key,而此时的A[j+1]就是上一轮的A[j](因为每次j都会-1)

而上一轮的A[j]已复制到上一轮的A[j+1]也就是当前的A[j+2]

所以我们可以直接令A[j+1]=key,不会丢失任何数据

只要我们持续这个操作,右手的所有牌最终都会正确地到达左手的正确位置

如何形式化的验证算法正确性:

算法导论给我们提供了一个方法,类似数学归纳法--“循环不变式”

关于循环不变式的详解:参考docs/循环不变式.md

我们来证明插入排序的循环不变式

如何找循环不变式?

由于算法是一步步执行的,那么如果每一步(包括初始和结束)都满足一个共同的条件,那么这个条件就是要找的循环不变式(loop invariant)

显然的,我们一直在试图维护左手牌堆是有序的这个性质
那么,插入排序的循环不变式就是:循环中,A[0…j-1]是有序的

接下来,我们证明循环不变式的三条性质:

1.初始化:显然,j = 1时,A[0…j-1]就是A[0],只有一个数字(这里我们提前将一张右手牌放入左手,并不影响结果),当然是符合循环不变式的
2.保持:若 A[0..j-1] 有序,将 A[j] 插入后,A[0..j] 仍有序
3.终止:终止时,当 j = n 时,A[0..n] 整体有序,排序完成

因此,插入排序最终被证明为正确的

关于代码实现,参考GitHub:《算法导论》笔记src/insertion_sort(插入排序).h

P.S.在之后的算法设计文档中,我们都使用循环不变式来设计和证明算法
文章及代码已同步到GitHub:《算法导论》笔记,欢迎参考
如有错误,请不吝赐教

内容概要:本文详细介绍了“秒杀商城”微服务架构的设计与实战全过程,涵盖系统从需求分析、服务拆分、技术选型到核心功能开发、分布式事务处理、容器化部署及监控链路追踪的完整流程。重点解决了高并发场景下的超卖问题,采用Redis预减库存、消息队列削峰、数据库乐观锁等手段保障数据一致性,并通过Nacos实现服务注册发现与配置管理,利用Seata处理跨服务分布式事务,结合RabbitMQ实现异步下单,提升系统吞吐能力。同时,项目支持Docker Compose快速部署和Kubernetes生产级编排,集成Sleuth+Zipkin链路追踪与Prometheus+Grafana监控体系,构建可观测性强的微服务系统。; 适合人群:具备Java基础和Spring Boot开发经验,熟悉微服务基本概念的中高级研发人员,尤其是希望深入理解高并发系统设计、分布式事务、服务治理等核心技术的开发者;适合工作2-5年、有志于转型微服务或提升架构能力的工程师; 使用场景及目标:①学习如何基于Spring Cloud Alibaba构建完整的微服务项目;②掌握秒杀场景下高并发、超卖控制、异步化、削峰填谷等关键技术方案;③实践分布式事务(Seata)、服务熔断降级、链路追踪、统一配置中心等企业级中间件的应用;④完成从本地开发到容器化部署的全流程落地; 阅读建议:建议按照文档提供的七个阶段循序渐进地动手实践,重点关注秒杀流程设计、服务间通信机制、分布式事务实现和系统性能优化部分,结合代码调试与监控工具深入理解各组件协作原理,真正掌握高并发微服务系统的构建能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值