IT技术博客大学习 共学习 共进步

算法

共 606 篇文章

IT 2013-07-07 21:46:52 / 累计浏览 2,256

通信复杂度问题:确定双方手中所有数的中位数

这篇讲的是通信复杂度中一个经典问题的算法设计:两地的两个人各自持有一个数字集合,他们想用尽可能少的通信来共同确定两个集合合并后的中位数,而不是笨拙地传输全部数据。 作者从问题背景出发,清晰地展示了从“暴力传输”到“极精巧算法”的优化路径。最初,A把全部数据传给B,通信量是O(n)比特;随后,通过在区间上进行多轮二分查找,算法被优化至O(log²n)。文章的高潮在于将复杂度降至O(logn)的精妙改进。核心思路是让双方比较各自数列的中位数a和b,根据大小关系丢弃一半不可能成为答案的数据。但关键在于,双方不需要完整传输a和b的数值,而是从高位到低位逐位比较并发送比特。一旦某一位不同就能立即停止本轮比较;同时,由于数据集在每轮都被减半,之前比过的公共前缀位在未来轮次中可以直接跳过。这种逐位比较与信息复用,将总通信量压到了O(logn)。 文章不仅给出了算法,还从信息论角度推导了Ω(logn)的下界,完整勾勒出问题的解空间。其价值在于,通过一个看似简单的游戏,生动展现了通信复杂度的理论力量与算法设计的艺术。

IT 2013-06-17 23:45:27 / 累计浏览 4,132

cpp智能指针的简单实现

这篇讲的是如何从零开始实现一个C++智能指针。文章直指C++没有垃圾回收导致的内存管理痛点——手动配对`new`和`delete`极易出错和内存泄漏,随后切入智能指针如何通过引用计数来自动化这一过程。 作者的实现核心在于一个巧妙的设计:使用一个在栈上自动管理生命周期的包装类`Ptr`,其内部持有一个指向堆上辅助类`SmartPtr`的指针。这个辅助类负责记录引用计数和实际的数据。当`Ptr`对象被复制时,引用计数增加;当其析构时,引用计数减少。只有当引用计数降为零,意味着没有`Ptr`实例再指向它时,辅助类及其管理的内存才会被释放。 这种“栈对象包装堆对象”的思路,正是智能指针将语言特性(栈自动回收)与手动内存管理相结合的关键。文章通过一段可运行的代码清晰地展示了引用计数的增减时机,让抽象的原理变得直观。理解了这个手动实现,也就真正理解了智能指针在C++内存模型中的运作基石。

IT 2013-06-02 19:43:32 / 累计浏览 3,749

由原子操作引起的关于Cache的讨论

这篇讲的是一个实际的性能排查案例:在MPI集群上,当PLDA算法与MLR或PLSA同时运行时,后者效率会大幅下降。问题最初被指向PLDA中频繁使用的原子操作——`lock incl`指令。用户担心这个`lock`前缀会锁死内存总线,拖垮整台机器。 作者澄清了一个常见误解:在现代CPU(如Nehalem架构)上,`lock`前缀在绝大多数情况下并不会锁总线,而是通过一种被称为“cache lock”的机制,在cache line级别实现原子性。他结合Intel手册与同行讨论,进一步指出硬件上并不存在真正的“cache lock”,而是依赖MESI这类缓存一致性协议来保证原子性。例如,带有写意图的原子读操作会触发RFO,导致其他核心的相关缓存失效,但这并不等同于锁住整个总线。 基于这个理解,问题的优化方向就清晰了:为了最小化不同任务之间的干扰,可以通过cgroup将它们绑定到不同的物理CPU上,从而隔离L1缓存。最终,作者通过共享内存和原子操作,替代了原先为每个线程分配独立大内存的做法,得以在限制内存占用的同时,启动更多线程将CPU利用满,反而获得了整体性能的提升。 对读者而言,这是一次从具体现象深入到底层硬件原理(CPU缓存一致性协议)的实用分析,有助于理解并发编程中原子操作的真实开销与优化思路。

IT 2013-06-02 19:42:39 / 累计浏览 6,657

无锁HashMap的原理与实现

这篇讲的是如何绕过传统锁机制,实现一个能在多线程环境下高性能运行的HashMap。作者从Java中HashMap的线程安全痛点出发,指出常用的锁替代方案都存在性能或复杂性问题,从而引出了基于CAS(比较并交换)指令的无锁编程思路。 文章的核心是清晰地拆解了无锁HashMap的实现蓝图。它先带你理解了更基础的无锁链表如何利用CAS保证插入和删除操作的原子性,然后直面HashMap最大的挑战——ReHash(扩容)。作者巧妙地借鉴了“分裂有序链表”的思想,通过一种预先对节点哈希值进行位翻转排序并引入哨兵节点的方法,让整个链表在逻辑上始终有序。这样一来,数组扩容时节点只需要确认自己在新链表中的归属,而无需物理移动,从而破解了传统实现中需要同时原子修改多个指针的难题。 此外,文章还提到了为了提升效率而采用的数组懒加载、分块初始化等工程细节。整体而言,这是一篇从原理到实现都讲解得非常扎实的文章,把一个复杂的并发数据结构设计问题,梳理得条理分明。

IT 2013-05-28 22:24:03 / 累计浏览 5,153

如何计算两个文档的相似度(二)

这篇系列文章的第二部分聚焦于gensim的实战上手。作者从安装这个看似简单的步骤切入,详细记录了在Ubuntu和Mac OS上配置gensim及其依赖库NumPy、SciPy时遇到的典型问题——比如Mac上因缺失Fortran编译器导致的SciPy安装失败,并给出了解决方案(通过Homebrew安装gfortran),这对国内开发者很有参考价值。 在核心的使用演示部分,文章没有照搬官方教程,而是另辟蹊径,使用了“Latent Semantic Indexing (LSI) A Fast Track Tutorial”中的三个简短英文文档作为案例。整个流程清晰展示了从文本预处理(小写化)、构建词袋字典、生成文档向量,到训练TF-IDF模型,最终通过LSI(潜在语义分析)将文档映射到二维主题空间的全过程。作者特别指出了gensim在计算IDF时未对出现频率为100%的词(如介词a, in, of)进行平滑处理导致其权重为零的现象,并以此反向论证了TF-IDF算法在过滤停用词上的有效性。 通过这个从安装到模型输出的完整闭环,文章为读者提供了一份可复现的gensim入门实践指南,为后续在“课程图谱”上的应用打下了基础。

IT 2013-05-28 22:24:02 / 累计浏览 6,698

如何计算两个文档的相似度(一)

作者在构建“课程图谱”网站时,面临课程推荐系统冷启动的难题:缺乏用户行为数据,人工标注标签又耗时。一个可行的思路是直接利用课程文本内容计算相似度,而作者最终选择了基于主题模型的自动化方案。 核心工具是强大的Python库gensim,文章以LSI(浅层语义索引)模型为例,展示了如何将两篇文档映射到主题维度,进而计算其语义相似度。作者用不到百行的代码便实现了这一流程,并给出了以Andrew Ng《机器学习》课为示例的推荐效果图。文章还规划了进一步优化:利用全量英文维基百科语料,在普通笔记本电脑上训练更复杂的LSI和LDA模型,以提升相似度计算效果。 文章整体脉络清晰,分为三个部分:先简要铺垫TF-IDF、SVD等基础知识点并提供参考资料;再详解gensim的安装与具体实现;最后探讨在大规模语料上训练模型的应用。作者并非平铺直叙,而是从实际项目需求出发,分享了从选型到落地的完整思考与实践。

IT 2013-05-20 23:30:33 / 累计浏览 2,949

算法分析中递推式的一般代数解法

这篇讲的是算法分析中一个关键但常被绕开的数学工具:如何用特征方程法,系统地求解递推关系式。文章作者直接切入,以经典的汉诺塔问题 T(n) = 2T(n-1) + 1 为例,演示了如何将这个递推式转化为封闭形式 T(n) = 2ⁿ - 1 的完整代数过程。 这种技巧是理解递归算法(如分治法)真实复杂度的基石。它超越了仅靠“主定理”快速查表的层面,让你能亲手推导并理解那些复杂度结论的由来。文章不仅给出了步骤,更重要的是展现了从递归定义到显式公式的逻辑链条,让你在遇到新的递推关系时,能有章可循地求解,而不仅仅是猜测或依赖现成结论。 对于需要分析自定义算法复杂度,或想更深入理解算法导论中数学原理的开发者来说,这篇文章提供了一套清晰、可操作的代数解法框架。

IT 2013-05-19 23:32:29 / 累计浏览 4,116

文件系统的树形结构改善构思

传统树形文件系统在管理一个文件属于多个分类时显得力不从心。要么得在多个目录下复制文件,造成空间浪费,要么就得忍受层级嵌套带来的繁琐点击。作者从这一痛点出发,提出了一个结合“标签”的改进思路。 核心方案是为文件系统增加一个自动生成的标签区。当文件被放入某个目录时,系统会为其打上该目录名的标签,同时也支持用户手动添加其他标签。这样,像《C/C++编程指南》这样的书籍就能同时关联“C语言”和“C++语言”两个标签,无需重复存储。 文章进一步探讨了两种访问逻辑:传统的树形目录用于文件管理,而标签则提供了另一种扁平化的快速索引路径。作者还设想了更人性化的交互——点击一个标签,文件管理器能直接打开包含该标签内容的相关目录,让访问路径更直观。 这篇构思主要在于抛砖引玉,探讨如何融合目录与标签的长处,来改善文件管理的体验。

IT 2013-05-17 13:47:17 / 累计浏览 5,879

经典证明:任意三角形都能被分成n≥4个等腰三角形

这篇讲的是一个经典的几何分割问题:如何把任意一个三角形分成任意多个(n≥4)等腰三角形。问题源自1976年的数学期刊,而1977年由Gali Salvatore给出了一套非常巧妙的通用证明。 作者从最基础的分割方法入手:先作高线把原三角形分成两个直角三角形,再对每个直角三角形作斜边中线,即可得到4个等腰三角形。基于这个“单元”,通过递归地对其中一个等腰三角形进行同样操作,就能以每次+3的增量,解决所有形如 n=4, 7, 10, 13… 的情况。 证明的关键在于处理 n=5 和 n=6 这两个“基例”。n=6 的方案相对直接,而 n=5 则需要一个巧妙的“预留一个等腰三角形,再把剩余部分分成四份”的策略。值得一提的是,这种方法无法直接用于等边三角形,文章为此专门展示了等边三角形分割成5个等腰三角形的独立方案,体现了思考的完备性。 整个证明思路层层递进,从通用构造到处理特殊情况,将一个看似复杂的问题分解为几个清晰可操作的步骤,展现了数学构造中的严谨与美感。

IT 2013-05-01 22:59:07 / 累计浏览 6,452

更快的IP库查找方法以及AWK中的二分查找

这篇讲的是如何用AWK高效处理IP地址定位问题。作者从实际工作中需要将大量用户IP映射到国家的需求出发,手头有12.5万条IP段的数据库和4万条用户记录。 起初,作者采用最直观的遍历方法,即对每个用户IP逐一扫描整个IP库进行匹配。虽然逻辑简单,但面对这个规模的数据,处理耗时长达40分钟,效率堪忧。 为了提速,作者在AWK中引入了二分查找算法。核心思路是将IP段数据预先加载到数组,然后对每个用户IP,通过比较中点值快速缩小搜索范围,而不是逐条检查。这个改造带来了惊人的性能提升:全部检索时间从40分钟骤降至不到2分钟,效率提高了20倍。 这个案例不仅解决了具体问题,也打破了作者原先认为“在AWK这类脚本语言中不适合实现算法”的固有印象,展示了选择合适算法对数据处理效率的决定性影响。

IT 2013-05-01 22:35:53 / 累计浏览 3,910

google group varint 无损压缩解压算法的高效实现 改进版

这篇讲的是Google Group Varint无损压缩解压算法的一个高效C++实现,作者在此前版本的基础上进行了关键优化,使整体性能提升了约20%。 文章的核心亮点在于那个256行的静态索引表。传统的Varint解码需要逐个判断每个字节的编码长度,而Group Varint将四个整数打包,通过一个字节的索引就能“一表查四数”,直接确定这组整数在压缩流中的起始位置和各自的长度。这种查表法将解码操作从逐位判断提升为批量定位,是速度飞跃的关键。 优化后的效果非常惊人:处理100万个整数(4MB数据),压缩耗时约3.2毫秒,解压仅需3.7毫秒。换算下来,其解压吞吐量可以达到每秒1GB,非常适合对速度要求极高的场景。文章不仅展示了性能对比,还直接提供了完整的实现源码,包括那个精心构造的索引表,开发者可以即拿即用。 对于追求极致性能的系统工程师而言,这个实现展示了如何通过巧妙的数据结构设计,将算法理论上的优势转化为实际运行效率的巨大提升。

IT 2013-05-01 17:40:52 / 累计浏览 5,468

简单理解Memcached的Slab Allocation

这篇讲的是Memcached如何用Slab Allocation机制管理内存。作者从内存分配的基本问题切入,解释了这套机制的核心:它预先将内存划分为大小固定的Page(默认1MB),再将每个Page切成相同尺寸的Chunk,相同尺寸的Chunk集合就构成了一个Slab。 这种预分配和分组的方式,能有效减少动态分配内存时产生的碎片。文章进一步指出,通过启动参数`-f`可以调整Growth Factor(默认1.25),它决定了相邻Slab中Chunk尺寸的倍增关系。不过,Slab Allocation并非完美:当实际数据尺寸与Chunk大小不匹配时,会产生内部碎片;当一个Slab无法被其Chunk大小整除,或是某些尺寸的Slab根本没被使用时,也会造成内存浪费。 文章通过结构图和工具截图,直观展示了Slab、Page与Chunk的关系,以及Growth Factor对不同Slab中Item尺寸的影响,清晰剖析了这个内存管理方案的利与弊。

IT 2013-04-07 13:18:50 / 累计浏览 7,504

深入浅出插入类排序算法(直接插入, 折半插入, 希尔排序)

这篇讲的是如何用最生活化的方式理解插入类排序。作者从打扑克牌的场景切入——手里每拿到一张新牌,就按大小插到已排好序的牌列中——清晰展示了直接插入排序的核心过程。文章不仅给出了这个算法的C语言实现,还通过统计比较和移动次数,分别演示了最好情况(已排序)和最坏情况(逆序)下的性能差异,最后得出平均时间复杂度为O(n²)的结论。 随后,文章对比介绍了两种优化变体。折半插入排序通过二分查找减少比较次数,而不是顺序扫描;希尔排序则通过分组和逐步缩小增量的方式,让排序效率更高。作者通过扑克牌的连续操作步骤,将这些抽象算法的每一步都可视化,让读者能直观看到算法如何一步步移动元素、缩小比较范围。 整体而言,这篇文章没有停留在代码罗列,而是结合实例与原理分析,把三种容易混淆的插入排序讲得层次分明。尤其适合想搞清楚“它们到底有什么不同”以及“各自适合什么场景”的学习者。

IT 2013-04-07 13:17:17 / 累计浏览 7,033

深入浅出交换类排序算法(冒泡排序,快速排序)

这篇文章通过对比两种经典的交换类排序算法,系统讲解了冒泡排序与快速排序的原理、实现与性能差异。文章从冒泡排序“两两比较、逐步上浮”的直观思想切入,用军训排队的生活比喻和具体的序列交换过程(如4,3,5,6,2,1的逐步排序),清晰展示了其简单但低效的特点,并指出平均时间复杂度为O(n²)。 重点转向快速排序时,文章突出了其“分治”核心:通过选取基准元素(pivot),用双指针扫描将序列划分为左右两部分,再递归处理。文中通过图示详细拆解了一轮快排的完整指针移动与交换过程,代码实现也体现了递归思想的巧妙运用。对比部分明确指出,快速排序在平均情况下时间复杂度可达O(n log n),是内排序中效率最高的算法之一,尤其适合处理大规模或基本无序的数据。 整体上,文章将算法思想、步骤图解、代码实现与复杂度分析紧密结合,既解释了冒泡排序易于理解但效率有限的教学价值,也阐明了快速排序在追求效率的实际应用中的优势。

IT 2013-04-07 13:15:28 / 累计浏览 4,593

深入浅出选择类排序算法(简单选择排序,堆排序)

这篇文章讲的是两种经典的选择类排序算法:简单选择排序与堆排序。作者从基本思想出发,用一个有趣的比喻——在班级中依次选择最喜欢的“女朋友”——生动解释了简单选择排序如何工作:每一轮遍历未排序部分,找出最小值放到已排序序列末尾。它的实现直观,但时间复杂度固定为O(n²),无法避免大量的比较操作。 堆排序的讲解则更深入。它将待排序序列构建成一个完全二叉树(大顶堆或小顶堆),利用“父节点总是最大(或最小)”的性质,通过反复交换堆顶元素与末尾元素并调整堆来完成排序。文中详细演示了建堆和递归调整的过程,并附有代码。这种基于二叉树结构的方法,将最坏情况下的时间复杂度提升到了O(nlogn),同时保持O(1)的空间复杂度。 两者虽然同属选择排序范畴,但效率差异显著。简单选择排序逻辑简单、易于实现,适合数据量小或对稳定性有要求的场景;堆排序则凭借更优的时间复杂度,在处理大规模数据时表现突出,是许多高效排序系统的基础。

IT 2013-04-07 13:14:34 / 累计浏览 4,028

数学中的极限思想求时间复杂度

这篇讲的是如何用更严谨的数学方法来计算算法的时间复杂度。通常大家习惯直接保留函数中的最高次项,比如将 T(n) = 2n^5 + 3n^2 - 1 简写为 O(n^5)。文章指出,这种简便做法其实不够严格,其背后的正确依据应当是数学中的极限概念。 作者详细推导了极限判定法:当 n 趋向无穷大时,T(n) 除以某个猜想 f(n) 与一个常数系数的乘积,若结果为一个常数,则 f(n) 就是 T(n) 的时间复杂度。用这个方法重新计算开头的例子,可以严谨地得出 O(n^5) 的结论。 更重要的是,极限思想能解决一些常规方法棘手的对比。比如比较多项式 n^k 和指数 k^n(k 为大于1的常数)的复杂度,直接判断很模糊。文章通过对两边取对数再求极限,清晰地证明 n^k / k^n 的极限为 0,从而得出结论:当 n 足够大时,O(n^k) 的效率远优于 O(k^n)。最后,文章还展示了如何将指数增长的思想应用于分析循环次数,推导出类似 i = i*2 这种模式的时间复杂度为 O(log₂n)。 这篇内容为理解算法效率提供了一个更坚实的数学视角,帮助开发者不仅知其然,更知其所以然。

IT 2013-04-06 23:19:04 / 累计浏览 4,498

信用卡校验位算法THE LUHN MOD-10

这篇讲的是信用卡卡号校验位背后的经典算法——Luhn Mod-10。作者从卡号的每一位数字出发,解释了如何通过一套简单而精巧的步骤来验证其真实性。 算法核心流程很清晰:先给卡号每位数字乘上一个交替的1或2的权重(具体从哪个权重开始,取决于卡号总位数是奇数还是偶数),如果乘积大于9则减去9。接着,将所有处理后的数字相加,最后对10取模。只要结果为0,就说明这个卡号格式上是校验通过的;否则很可能存在输入错误或是伪造号码。 这个算法的巧妙之处在于,它仅通过基础的加减法和取模运算,就能高效地检测出常见的输入错误,比如输错一位数字或相邻两位数字颠倒。虽然简单,但它被广泛应用在银行卡、身份号码等多种编码体系中,作为一种可靠的基础校验手段。

IT 2013-04-06 23:16:11 / 累计浏览 7,889

记一下那些伪随机数生成函数

这篇讲的是一个在多线程环境下由伪随机数生成函数引发的性能“血案”。作者在使用100M数据跑LDA算法时,发现多线程迭代反而比单线程慢了数倍,且CPU大量时间耗在系统空间。排查后发现,罪魁祸首正是代码中调用的random()函数。 原来,glibc中的random()和rand()虽然是线程安全的,但其内部实现(加了一把全局锁)导致它们是不可重入的。在多线程竞争同一把锁时,性能便一落千丈。文章进而对比了rand_r()等可重入函数的实现,指出其算法较弱;而drand48系列函数则提供了更优的、基于独立状态数组的可重入版本(如erand48_r())。作者清晰地梳理了这些函数族的内在区别与适用场景:若需多线程高性能计算,必须使用能维护线程本地状态的可重入版本。 文章从具体坑点出发,剖析了glibc源码实现,最终落到了对随机数函数选型的实用建议上,对理解并发编程中的隐形陷阱很有启发。

IT 2013-04-06 23:13:26 / 累计浏览 5,290

为什么Fibonacci数列相邻两项之比会趋于0.618?

这篇文章从斐波那契数列相邻两项的比值现象出发,解释了它为何会趋近黄金比例0.618。作者没有停留在代数推导上,而是借助 Mathematica 动画,通过“黄金矩形”的几何构造给出了一个直观而优美的解释。 核心思路是:以斐波那契数对(1,1)、(1,2)、(2,3)…为边长构造矩形序列,每个新矩形都由前一个旋转90度并拼接一个正方形生成。随着图形不断递归生长,初始细节的影响逐渐消散,矩形整体与其内部的小矩形变得高度相似——这正是黄金矩形的定义特征。图形化演示清晰地展现了,无论起始矩形的比例如何,最终都会收敛到相同的黄金比例。 文章还顺带点明了一个有趣的结论:任何遵循“每项为前两项之和”规律的数列(哪怕起始值是任意的2和7),其相邻项比值最终都会收敛到0.618。这让抽象的数学规律变得可看、可感,也解释了一个常见的数学魔术背后的原理。

IT 2013-04-06 22:59:21 / 累计浏览 3,126

思考题:如下场景如何设计mongo collection

这篇探讨的是一个实际场景下的 MongoDB 集合设计问题:如何记录用户每次登录的业务标识、IP 及时间,并高效查询特定用户在特定业务下的某个 IP 是否已存在。 作者给出了两种思路并进行了对比。方案一采用扁平结构,为每次登录记录一条独立文档,结构清晰,查询时通过 `findOne` 直接定位。方案二则将同一用户同一业务下的所有登录记录聚合到一个文档中,将 IP 和时间存储为子文档数组,这在查询特定 IP 时语法略显复杂。 文章的核心矛盾点在于如何权衡查询的便捷性与应对新需求的灵活性。当需求变更为“每个用户同一业务只保留最新5条记录”时,方案一需要额外的计数与删除操作,而方案二则更利于在应用层(如 PHP)对数组进行裁剪后整体更新。作者最终选择了后者,并通过客户端脚本管理数组元素,再使用 `upsert` 操作同步回数据库。这反映了在 NoSQL 设计中,有时需要结合应用逻辑来弥补数据库层功能的不足。